← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.10.8+0.10.9
Cargo.lock+1−1
@@ -122,7 +122,7 @@ dependencies = [  [[package]] name = "swx-bridge"-version = "0.10.8"+version = "0.10.9" dependencies = [  "miniz_oxide",  "serde",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.8"+version = "0.10.9" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+28−1
@@ -498,7 +498,34 @@ not.  ## Roadmap -- v0.10.8 (this): **clear_part in passes.** 0.10.7's full live self-test+- v0.10.9 (this): **the presence rule, made symmetric.** Measured+  2026-10-14 on hosted 0.10.7, with Drew at the keyboard during a battery:+  - 17 `swx_reset_workspace` calls in a row were refused for his presence;+  - 18 new scratch parts were allowed and piled up on his screen;+  - checks slowed to a 7-minute stall, SOLIDWORKS reached 15.1 GB, and the+    machine 91 % committed.++  Now:+  - **Creation is guarded too.** While the user is at the machine (input+    within the idle threshold), the self-test's write level and+    `purpose:"bridge"` documents and scripts wait for the user to go idle (up+    to `waitForIdleSeconds`, default 120), then refuse with `user_present`+    having created nothing. A user's own script is still allowed, and its+    reply says the user is active.+  - **Closing the bridge's own scratch is allowed while the user is present,**+    except the active document. `swx_reset_workspace` reports `leftActive`. It+    is refused only when no document qualifies, and each close is ledgered+    with `userPresent:true`.+  - **The self-test stops** (`stoppedFor:"userPresent"`) before a check if the+    user is still active after the wait. It closes its own part first and+    never ploughs on.+  - **`swx_run_script` reports `documentsLeftOpen`** with `why:"user_present"`+    when it made a document while the user was active.+  - **`swx_status.userPresence`** (`active`, `idleSeconds`, `foreground`) is+    always present, read without COM.++  635 tests.+- v0.10.8: **clear_part in passes.** 0.10.7's full live self-test   completed:   - nothing was throttled, and the launch was attributed by `ancestry`;   - it created 15 documents instead of more than 32;
SKILL.md+14−5
@@ -88,11 +88,20 @@ BEFORE you do it: 8. **`swx_stop` quits only a session with zero documents open**, and `force`    never waives that. Treat a quit as close to a one-way door: relaunching takes    minutes and can stop at the sign-in window again.-9. **Never end a session the user is working in.** 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; the session is then theirs.-   `userPresentOverride` is for the user's explicit request only — never a-   workaround.+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;+   the session is then theirs. `userPresentOverride` is for the user's explicit+   request only — never a workaround. The rule runs both ways (0.10.9):+   - While the user is at the machine (`swx_status.userPresence.active`), the+     bridge does not create documents for its OWN purposes. That covers+     `swx_selftest`, and `purpose:"bridge"` on `swx_new_document` and+     `swx_run_script`. Those wait up to `waitForIdleSeconds`, then refuse with+     `user_present`.+   - `swx_reset_workspace` still closes the bridge's own scratch while the user+     is present, except the active document (`leftActive`).+   - When a script reports `documentsLeftOpen`, stop and close them. Never keep+     going. Measured: 18 scratch windows piled up on Drew's screen.  ## The everyday flow 
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "solidworks",   "displayName": "SOLIDWORKS",-  "version": "0.10.8",+  "version": "0.10.9",   "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",
install.sh

  
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "solidworks-bridge",-  "version": "0.10.8",+  "version": "0.10.9",   "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-memory/SKILL.md+15
@@ -65,6 +65,21 @@ Parent skill: **solidworks-bridge**   1772 MB, 285 MB more than before it was created. This happened once and has no   explanation yet; record it if you see it again. +## The incident that made the presence rule symmetric++- [measured] 2026-10-14, hosted 0.10.7, Drew at the keyboard (closing Fusion) during a+  battery. From 23:19:32Z every `swx_reset_workspace` was refused for his presence (17 in a+  row), while every `swx_run_script` with `new_part` was allowed (18 parts). 18 of the+  bridge's unsaved scratch windows piled up on his screen.+- Check times went from 14 s to 39 s, then a 7-minute stall. SOLIDWORKS reached+  15.1 GB, the machine reached 91 % committed, and the session could not be quit the next+  morning, because documents were still open.+- From 0.10.9 the rule runs both ways. While the user is at the machine, the bridge+  does not create documents for its own purposes: the self-test and batteries wait,+  then refuse with `user_present`. `swx_reset_workspace` closes the bridge's own scratch+  even while the user is present, except the active document. A battery that sees+  `documentsLeftOpen`, or a refused close, stops.+ ## The rules  1. **Read memory before a long run, and between its steps.** `swx_status` reports a
src/launchos.rs+9
@@ -1309,6 +1309,15 @@ impl LaunchOs for FakeOs {         if self.dir.join("presence-unreadable").exists() {             return Err("simulated: the last-input time could not be read".into());         }+        // `user-arrives-after-documents`: N (0.10.9) — the user sits down once+        // the simulated SOLIDWORKS has created N documents, so a test can make+        // presence change in the middle of a run.+        if let Some(n) = std::fs::read_to_string(self.dir.join("user-arrives-after-documents")).ok().and_then(|t| t.trim().parse::<u64>().ok()) {+            let made = std::fs::read_to_string(self.dir.join("documents-created")).ok().and_then(|t| t.trim().parse::<u64>().ok()).unwrap_or(0);+            if made >= n {+                return Ok(5);+            }+        }         Ok(std::fs::read_to_string(self.dir.join("last-input-secs"))             .ok()             .and_then(|t| t.trim().parse().ok())
src/presence.rs+149−11
@@ -44,13 +44,154 @@ pub fn idle_secs() -> u64 { }  /// The verbs that end a user's work, and so are guarded.-pub const GUARDED: &[&str] = &[-    "swx_stop",-    "swx_shutdown",-    "swx_close_all_safely",-    "swx_close_all_documents",-    "swx_reset_workspace",-];+///+/// `swx_reset_workspace` is NOT here (0.10.9): it closes only the bridge's own+/// never-saved scratch documents, and while the user is present it does so for+/// every one EXCEPT the active document (see `verbs3::reset_workspace`).+/// MEASURED 2026-10-14: with it refused outright, 17 resets in a row were+/// refused while 18 new scratch parts were allowed — 18 of the bridge's windows+/// piled up on the user's screen.+pub const GUARDED: &[&str] = &["swx_stop", "swx_shutdown", "swx_close_all_safely", "swx_close_all_documents"];++/// The explicit user-present override: `userPresentOverride:true` ++/// `userAuthorized:true` + a reason.+pub fn overridden(args: &Value) -> bool {+    args.get("userPresentOverride").and_then(|v| v.as_bool()) == Some(true)+        && args.get("userAuthorized").and_then(|v| v.as_bool()) == Some(true)+        && args.get("reason").and_then(|v| v.as_str()).map(|r| !r.trim().is_empty()).unwrap_or(false)+}++// ── IS THE USER AT THE MACHINE RIGHT NOW? (0.10.9) ───────────────────────────+//+// MEASURED 2026-10-14 (hosted 0.10.7): while Drew was at the keyboard, every+// close the bridge attempted was refused for his presence and every NEW scratch+// part was allowed — 18 of the bridge's windows piled up on his screen, checks+// went from 14 s to a 7-minute stall, SOLIDWORKS reached 15.1 GB and the+// machine 91 % committed. The rule is now symmetric: the bridge does not END a+// session the user is working in, and it does not POP its own windows on a+// user who is working.++/// The user's ACTIVITY, read without COM: input within `idle_secs()`+/// system-wide (GetLastInputInfo) is a user at the machine, whatever+/// application is in front. An unreadable input time counts as active.+#[derive(Clone, Debug)]+pub struct Activity {+    pub active: bool,+    pub idle_seconds: Option<u64>,+    pub foreground: Value,+    pub foreground_is_solidworks: Option<bool>,+}++impl Activity {+    pub fn to_json(&self) -> Value {+        json!({+            "active": self.active,+            "idleSeconds": self.idle_seconds,+            "idleThresholdSecs": idle_secs(),+            "foreground": self.foreground,+            "foregroundIsSolidworks": self.foreground_is_solidworks,+            "rule": "active = the user gave input (anywhere on the machine) within idleThresholdSecs; \+                     an unreadable input time counts as active. While active, the bridge does not \+                     create documents for its own purposes (self-test, batteries) and does not end \+                     the session.",+        })+    }+}++pub fn activity() -> Activity {+    let os = crate::launchos::os();+    // A simulated SOLIDWORKS with no presence scenario is on nobody's screen.+    if crate::is_mock() && std::env::var(crate::launchos::ENV_FAKE_DIR).map(|d| d.is_empty()).unwrap_or(true) {+        return Activity { active: false, idle_seconds: None, foreground: Value::Null, foreground_is_solidworks: None };+    }+    let input = os.seconds_since_last_input().ok();+    let sw_pids: Vec<u32> = os+        .processes()+        .ok()+        .map(|c| {+            let sw: Vec<u32> = c.iter().filter(|p| p.is("SLDWORKS.exe")).map(|p| p.pid).collect();+            let mut all = sw.clone();+            all.extend(c.iter().filter(|p| p.ppid.map(|pp| sw.contains(&pp)).unwrap_or(false)).map(|p| p.pid));+            all+        })+        .unwrap_or_default();+    let (foreground, is_sw) = match os.foreground_window() {+        Ok(Some(w)) => (json!({"title": w.title, "pid": w.pid}), Some(sw_pids.contains(&w.pid))),+        Ok(None) => (json!({"unreadable": "no window has the foreground"}), None),+        Err(e) => (json!({"unreadable": e}), None),+    };+    Activity {+        active: input.map(|n| n < idle_secs()).unwrap_or(true),+        idle_seconds: input,+        foreground,+        foreground_is_solidworks: is_sw,+    }+}++/// How long a bridge-purpose creation waits for the user to go idle:+/// `waitForIdleSeconds`, else `SWX_WAIT_FOR_IDLE_SECS`, else 120 (at most 900).+pub fn wait_for_idle_secs(args: &Value) -> u64 {+    args.get("waitForIdleSeconds")+        .and_then(|v| v.as_u64())+        .or_else(|| std::env::var("SWX_WAIT_FOR_IDLE_SECS").ok().and_then(|v| v.trim().parse().ok()))+        .unwrap_or(120)+        .min(900)+}++/// WAIT FOR THE USER TO GO IDLE, up to `secs`. `Ok` = idle (or became idle);+/// `Err` = still active when the time ran out — with the last reading.+pub fn wait_for_idle(secs: u64) -> Result<Activity, Activity> {+    let poll = std::time::Duration::from_millis(+        std::env::var("SWX_PRESENCE_POLL_MS").ok().and_then(|v| v.trim().parse().ok()).unwrap_or(5000),+    );+    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(secs);+    loop {+        let a = activity();+        if !a.active {+            return Ok(a);+        }+        if std::time::Instant::now() >= deadline {+            return Err(a);+        }+        std::thread::sleep(poll.min(deadline.saturating_duration_since(std::time::Instant::now()).max(std::time::Duration::from_millis(10))));+    }+}++/// A verb that would CREATE a visible document for the BRIDGE's own purposes:+/// the self-test's write level, and `swx_new_document` / `swx_run_script`+/// marked `purpose:"bridge"` (batteries, the bridge's own scratch). A user's+/// own request is not this.+pub fn creates_for_bridge(command: &str, args: &Value) -> bool {+    match command {+        "swx_selftest" => args.get("level").and_then(|l| l.as_str()) == Some("write"),+        "swx_new_document" | "swx_run_script" | "swx_import" => {+            args.get("purpose").and_then(|p| p.as_str()) == Some("bridge")+        }+        _ => false,+    }+}++/// The refusal for a bridge-purpose creation while the user is active.+pub fn user_present_refusal(verb: &str, waited: u64, a: &Activity) -> Value {+    let mut out = crate::verbs::err_code(+        "user_present",+        format!(+            "{verb} refused: the user is at the machine (input {} ago) and stayed active for the {waited} s \+             the bridge waited. It does not open its own windows on a user who is working. Nothing was created.",+            a.idle_seconds.map(|n| format!("{n} s")).unwrap_or_else(|| "at an unreadable time".into())+        ),+        "Run it when the user is away (swx_status.userPresence.active is false), or pass a longer \+         waitForIdleSeconds. A script the USER asked for is not refused — this is only for the \+         bridge's own documents (purpose:\"bridge\", batteries, the self-test).",+    );+    if let Value::Object(m) = &mut out {+        m.insert("userPresence".into(), a.to_json());+        m.insert("changedNothing".into(), json!(true));+        m.insert("waitedSeconds".into(), json!(waited));+    }+    crate::ledger::record("user_present_refusal", json!({"verb": verb, "kind": "creation", "waitedSeconds": waited}));+    out+}  fn path() -> std::path::PathBuf {     crate::scratch::root().join("presence.json")@@ -383,10 +524,7 @@ pub fn guard(ctx: &crate::verbs::Ctx, verb: &str, args: &Value) -> Result<Value,     if !present {         return Ok(evidence);     }-    let overridden = args.get("userPresentOverride").and_then(|v| v.as_bool()) == Some(true)-        && args.get("userAuthorized").and_then(|v| v.as_bool()) == Some(true)-        && args.get("reason").and_then(|v| v.as_str()).map(|r| !r.trim().is_empty()).unwrap_or(false);-    if overridden {+    if overridden(args) {         let who = crate::caller::current();         crate::ledger::record(             "user_present_override",
src/selftest.rs+45−2
@@ -177,6 +177,7 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {         // bridge started with nothing open (swx_stop, swx_start, ready), and         // otherwise STOPS — it never ends a session that is not its own.         let memory_override = crate::memory::override_reason(args).ok().flatten();+        let idle_wait = crate::presence::wait_for_idle_secs(args);         let mut memory_log = MemoryLog::default();         let mut last_check: Option<&'static str> = None;         // ONE VISIBLE DOCUMENT FOR THE BATTERY (0.10.7). MEASURED 2026-10-14:@@ -191,6 +192,11 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {             if !only.is_empty() && !only.iter().any(|o| o == check.id) {                 continue;             }+            if let Err(stop) = presence_between(ctx, last_check, check.id, idle_wait, &mut shared) {+                memory_log.stopped_for = Some("userPresent");+                memory_log.stopped = Some(stop);+                break;+            }             if let Err(stop) = memory_between(ctx, last_check, check.id, memory_override.as_deref(), &mut memory_log, &mut shared) {                 memory_log.stopped = Some(stop);                 break;@@ -220,6 +226,12 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {             last_check = Some(check.id);         }         close_shared(ctx, &mut shared);+        if memory_log.stopped.is_none() {+            if let Err(stop) = presence_between(ctx, last_check, "the verbs on a scratch part", idle_wait, &mut shared) {+                memory_log.stopped_for = Some("userPresent");+                memory_log.stopped = Some(stop);+            }+        }         if memory_log.stopped.is_none() {             if let Err(stop) = memory_between(ctx, last_check, "the verbs on a scratch part", memory_override.as_deref(), &mut memory_log, &mut shared) {                 memory_log.stopped = Some(stop);@@ -299,6 +311,8 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { /// What the memory checks between the geometry checks did. #[derive(Default)] struct MemoryLog {+    /// Why the run stopped early: "memory" (the default) or "userPresent".+    stopped_for: Option<&'static str>,     /// Documents the run created (the shared part counts once).     documents_created: usize,     /// Checks that ran under HIGH commit pressure with SOLIDWORKS within budget.@@ -322,7 +336,7 @@ impl MemoryLog {             "memory": crate::memory::report(crate::launchos::os().as_ref()),         });         if let Some(stop) = &self.stopped {-            v["stoppedFor"] = json!("memory");+            v["stoppedFor"] = json!(self.stopped_for.unwrap_or("memory"));             v["stopped"] = stop.clone();         }         if let Some(r) = over {@@ -332,6 +346,30 @@ impl MemoryLog {     } } +/// Before one geometry check: is the user at the machine? (0.10.9) Wait up to+/// `wait` seconds for them to go idle; if they do not, close the run's own+/// shared part (its scratch — never theirs) and STOP. Never plough on: the+/// measured alternative was 18 of the bridge's windows on Drew's screen.+fn presence_between(ctx: &Ctx, last: Option<&str>, next: &str, wait: u64, shared: &mut Option<String>) -> Result<(), Value> {+    match crate::presence::wait_for_idle(wait) {+        Ok(_) => Ok(()),+        Err(a) => {+            close_shared(ctx, shared);+            crate::ledger::record("user_present_refusal", json!({"verb": "swx_selftest", "beforeCheck": next, "waitedSeconds": wait}));+            Err(json!({+                "beforeCheck": next,+                "afterCheck": last,+                "why": format!(+                    "the user is at the machine (input {} ago) and stayed active for the {wait} s the run waited; \+                     the run closed its own scratch part and stopped rather than open more windows on them",+                    a.idle_seconds.map(|n| format!("{n} s")).unwrap_or_else(|| "at an unreadable time".into())+                ),+                "userPresence": a.to_json(),+            }))+        }+    }+}+ /// Before one geometry check: read memory, and recycle or stop if it calls /// for it. `Err` stops the run, with everything that explains why. fn memory_between(@@ -550,8 +588,13 @@ fn finish_with(level: &str, sw: &Value, ready: bool, running: bool, started: Ins     };     let lead = match extra.get("stopped") {         Some(stop) => Some(format!(-            "THE RUN STOPPED FOR MEMORY before {}: {}. Every row above that point is a real result; \+            "THE RUN STOPPED {} before {}: {}. Every row above that point is a real result; \              nothing after it ran. {}",+            if extra.get("stoppedFor").and_then(|v| v.as_str()) == Some("userPresent") {+                "BECAUSE THE USER IS AT THE MACHINE"+            } else {+                "FOR MEMORY"+            },             stop["beforeCheck"].as_str().unwrap_or("the next check"),             stop["why"].as_str().unwrap_or("memory"),             lead.unwrap_or_default()
src/verbs.rs+31−1
@@ -644,6 +644,9 @@ fn swx_status(ctx: &Ctx, _args: &Value) -> Value {         // share, and the largest holders — read-only facts. Read it before a         // long run (the solidworks-memory skill).         "memory": crate::memory::report(os.as_ref()),+        // IS THE USER AT THE MACHINE? (0.10.9) Read without COM, every time, so a+        // battery can check it in the same call as memory.+        "userPresence": crate::presence::activity().to_json(),         "windowEtiquette": "background",         "_hint": hint,     }))@@ -2026,7 +2029,7 @@ fn prime_session(         .and_then(|v| v.as_str())         .map(String::from);     crate::session_history::mark(id.pid, Some(id.created_at));-    let closed = crate::verbs3::reset_workspace(ctx, &json!({}));+    let closed = crate::verbs3::reset_workspace(ctx, &json!({"_presenceMeasuredByCaller": true}));     let after = ctx         .call("app_state", json!({}), 20)         .ok()@@ -3428,6 +3431,21 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value {             }         }     }+    // THE OTHER HALF OF THE PRESENCE RULE (0.10.9): the bridge does not POP+    // ITS OWN WINDOWS on a user who is working. A verb creating a document for+    // the bridge's own purposes waits for the user to go idle, then refuses+    // `user_present`, having created nothing. A user's own request is allowed+    // and its reply says the user is active.+    if crate::presence::creates_for_bridge(command, args) {+        let wait = crate::presence::wait_for_idle_secs(args);+        if let Err(a) = crate::presence::wait_for_idle(wait) {+            let mut refusal = crate::presence::user_present_refusal(command, wait, &a);+            if let (Some(summary), Value::Object(m)) = (at_gate.clone(), &mut refusal) {+                m.entry("readiness").or_insert(summary);+            }+            return refusal;+        }+    }     let changes_documents = is_known(command)         && startup_touch(command) == crate::readiness::Touch::Mutate         && !is_startup_exempt(command);@@ -3444,6 +3462,18 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value {     if let (Some(o), Value::Object(m)) = (memory_override, &mut out) {         m.entry("memoryOverride").or_insert(o);     }+    // A USER'S OWN document request, while they are active: allowed, and said.+    if matches!(command, "swx_new_document" | "swx_run_script" | "swx_import")+        && !crate::presence::creates_for_bridge(command, args)+    {+        let a = crate::presence::activity();+        if a.active {+            if let Value::Object(m) = &mut out {+                m.entry("userActive").or_insert(json!(true));+                m.entry("userPresence").or_insert(a.to_json());+            }+        }+    }     if changes_documents {         // A verb that adds documents on a session the user has taken over is         // allowed — it adds, never removes — and says so.
src/verbs3.rs+82−2
@@ -2933,7 +2933,7 @@ pub fn reset_workspace(ctx: &Ctx, args: &Value) -> Value {     // that are still open; without this the reported list (and the sweep) could     // name a title from a previous session — which, since SOLIDWORKS restarts     // its Part numbering, might now be the USER's document.-    let (session, open_docs, _active, truncated) = ownership_context(ctx);+    let (session, open_docs, active, truncated) = ownership_context(ctx);     prune_unless_truncated(ctx, session, &open_docs, truncated);     // A SWEEP THAT CANNOT SEE THE SESSION IS NOT A CLEAN SWEEP. With the open     // list unlistable this reported `success:true, closed:[], remaining:[]` while@@ -2968,6 +2968,23 @@ pub fn reset_workspace(ctx: &Ctx, args: &Value) -> Value {         a.iter().filter_map(|v| v.as_str()).map(String::from).collect()     });     let force = arg_bool(args, "force", false);+    // THE USER IS PRESENT (0.10.9): closing the bridge's OWN never-saved scratch+    // restores their screen and never touches their documents, so it goes on —+    // for every one EXCEPT the active document, which they may have clicked+    // into. MEASURED 2026-10-14: refusing the whole sweep while new scratch+    // parts were still allowed left 18 of the bridge's windows on Drew's screen.+    // `_presenceMeasuredByCaller` is the quit ladder's prime, which measured+    // presence itself before it made its one part (underscore keys never come+    // from a caller: the dispatcher strips them).+    let present: Option<Value> = if crate::presence::overridden(args)+        || args.get("_presenceMeasuredByCaller").and_then(|v| v.as_bool()) == Some(true)+    {+        None+    } else {+        crate::presence::measure(ctx).filter(|(p, _)| *p).map(|(_, e)| e)+    };+    let mut left_active: Vec<String> = Vec::new();+    let mut qualifying = 0usize;     let mut closed = Vec::new();     let mut skipped = Vec::new();     for doc in &open_docs {@@ -2980,6 +2997,11 @@ pub fn reset_workspace(ctx: &Ctx, args: &Value) -> Value {             skipped.push(json!({"title": title, "why": "not created by this bridge in this SOLIDWORKS session"}));             continue;         }+        if present.is_some() && active.as_deref() == Some(title.as_str()) {+            left_active.push(title.clone());+            skipped.push(json!({"title": title, "why": "ours, but it is the ACTIVE document and the user is present — they may have clicked into it"}));+            continue;+        }         if let Some(want) = &only {             if !want.contains(&title) {                 skipped.push(json!({"title": title, "why": "ours, but not named in titles[]"}));@@ -3030,9 +3052,13 @@ pub fn reset_workspace(ctx: &Ctx, args: &Value) -> Value {             }));             continue;         }+        qualifying += 1;         match ctx.call("close_doc", json!({"docName": title.clone(), "path": path, "save": false}), 60) {             Ok(_) => {-                crate::ledger::record("document_closed", json!({"title": title, "by": "reset_workspace"}));+                crate::ledger::record(+                    "document_closed",+                    json!({"title": title, "by": "reset_workspace", "userPresent": present.is_some()}),+                );                 ctx.forget_scratch_doc(&title, path.as_deref());                 closed.push(title);             }@@ -3047,6 +3073,28 @@ pub fn reset_workspace(ctx: &Ctx, args: &Value) -> Value {     // NULL, NOT `[]`, when the read failed or came back truncated. An empty     // array here reads as "nothing is left open", which is the one thing a     // failed read must never be allowed to say.+    if let Some(evidence) = &present {+        if qualifying == 0 {+            let mut out = err_code(+                "user_is_active",+                format!(+                    "swx_reset_workspace: the user is working in this SOLIDWORKS ({}), and no document qualifies — \+                     the only scratch document of the bridge's is the active one{}",+                    evidence.get("basis").and_then(|b| b.as_str()).unwrap_or("present"),+                    if left_active.is_empty() { " (and there is none)" } else { "" }+                ),+                "Nothing was closed. While the user is present, the bridge closes its own never-saved scratch \+                 documents EXCEPT the active one; it never touches their documents.",+            );+            if let Value::Object(m) = &mut out {+                m.insert("userPresence".into(), evidence.clone());+                m.insert("leftActive".into(), json!(left_active));+                m.insert("closed".into(), json!([]));+            }+            crate::ledger::record("user_present_refusal", json!({"verb": "swx_reset_workspace", "leftActive": left_active}));+            return out;+        }+    }     let remaining = match ctx.call("docs", json!({}), 30) {         Ok(r) if r.get("truncated").and_then(|v| v.as_bool()) != Some(true) => {             r.get("docs").cloned().unwrap_or(Value::Null)@@ -3054,6 +3102,8 @@ pub fn reset_workspace(ctx: &Ctx, args: &Value) -> Value {         _ => Value::Null,     };     ok(json!({+        "leftActive": left_active,+        "userPresent": present.is_some(),         "closed": closed,         "untitledScratchClosed": closed.len(),         "skipped": skipped,@@ -4760,6 +4810,8 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value {             if let (Value::Object(m), Some(v)) = (&mut out, settled) {                 m.insert("visibility".into(), v);             }+            let made = created_title_of(out.get("createdBeforeFailing"));+            note_left_open(&mut out, made);             out         }         Ok(mut r) => {@@ -4768,6 +4820,8 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value {             // was measured live on THIS SOLIDWORKS release — one table,             // compat::OP_FACTS (docs/LIVE-RESULTS.md).             attach_proofs(&mut r, &ops);+            let made = created_title_of(r.get("created").filter(|c| !c.is_null()));+            note_left_open(&mut r, made);             // HIDDEN new_part steps (0.10.6): the session setting, settled and             // ledgered per step.             if let Some(steps) = r.get_mut("steps").and_then(|s| s.as_array_mut()) {@@ -4825,6 +4879,32 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value {     } } +fn created_title_of(created: Option<&Value>) -> Option<String> {+    let c = created?;+    c.get("created")+        .and_then(|x| x.get("title"))+        .or_else(|| c.get("title"))+        .and_then(|t| t.as_str())+        .map(String::from)+}++/// NEVER ACCUMULATE SILENTLY (0.10.9): a script that made a document while the+/// user is at the machine says, in its reply, that the document is still open+/// and why — so a battery that cannot close it stops instead of piling windows+/// up (measured 2026-10-14: 18 of them on Drew's screen).+fn note_left_open(out: &mut Value, title: Option<String>) {+    let Some(title) = title else { return };+    let a = crate::presence::activity();+    if !a.active {+        return;+    }+    if let Value::Object(m) = out {+        m.insert("documentsLeftOpen".into(), json!([title]));+        m.insert("why".into(), json!("user_present"));+        m.insert("userPresence".into(), a.to_json());+    }+}+ /// `proven` on every step of a run_script reply, from compat::step_proof and /// the release the host reports (`solidworksRevision`). fn attach_proofs(r: &mut Value, ops: &[crate::script::Op]) {
tests/presence.rs+158
@@ -301,3 +301,161 @@ fn the_override_ledger_line_names_the_caller() {         s(&line)     ); }++// ── 0.10.9: THE SYMMETRIC RULE ──────────────────────────────────────────────+//+// MEASURED 2026-10-14 (hosted 0.10.7, Drew at the keyboard): every+// swx_reset_workspace was refused for his presence (17 in a row) while every+// new scratch part was allowed (18) — 18 of the bridge's windows piled up on+// his screen, a 7-minute stall, SOLIDWORKS at 15.1 GB, 91 % committed. The+// bridge now neither ends his session nor opens its own windows on him.++fn docs(b: &Bridge) -> Vec<String> {+    b.call("swx_docs", json!({}))["docs"]+        .as_array()+        .map(|a| a.iter().filter_map(|d| d["title"].as_str().map(String::from)).collect())+        .unwrap_or_default()+}++fn ledger(b: &Bridge) -> Vec<Value> {+    b.call("swx_action_ledger", json!({"limit": 500}))["actions"].as_array().cloned().unwrap_or_default()+}++fn box_script() -> Value {+    json!([{"op": "new_part"},+           {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},+           {"op": "extrude", "depthMm": 10}])+}++#[test]+fn the_bridge_does_not_open_its_own_windows_on_a_working_user() {+    let m = Sim::new("presence-create");+    let b = m.boot();+    m.foreground_other_app();+    m.last_input(5);+    let st = b.call("swx_status", json!({}));+    check!(+        b,+        "swx_status always carries userPresence: active, idle seconds, the foreground window",+        st["userPresence"]["active"] == true && st["userPresence"]["idleSeconds"] == 5+            && st["userPresence"]["foreground"]["title"] == "Inbox - Outlook",+        s(&st["userPresence"])+    );+    let before = docs(&b);+    for (verb, args) in [+        ("swx_new_document", json!({"type": "part", "purpose": "bridge", "waitForIdleSeconds": 0})),+        ("swx_run_script", json!({"script": box_script(), "purpose": "bridge", "waitForIdleSeconds": 0})),+        ("swx_selftest", json!({"level": "write", "waitForIdleSeconds": 0})),+    ] {+        let r = b.call(verb, args);+        check!(+            b,+            &format!("{verb} for the bridge's own purposes while the user works: user_present, nothing created"),+            r["errorCode"] == "user_present" && r["changedNothing"] == true && r["userPresence"]["active"] == true,+            s(&r)+        );+    }+    check!(b, "...not one document appeared", docs(&b) == before, format!("{before:?} -> {:?}", docs(&b)));+    check!(+        b,+        "the refusals are ledgered as creations refused",+        ledger(&b).iter().filter(|e| e["action"] == "user_present_refusal" && e["detail"]["kind"] == "creation").count() == 3,+        s(&json!(ledger(&b)))+    );+    // A USER'S OWN script is their business: allowed, and the reply says the+    // user is active and what it left open.+    let r = b.call("swx_run_script", json!({"script": box_script()}));+    check!(+        b,+        "a user's own script while they work: allowed, saying the user is active and naming the document it left open",+        r["success"] == true && r["userActive"] == true+            && r["documentsLeftOpen"].as_array().map(|a| a.len() == 1).unwrap_or(false)+            && r["why"] == "user_present",+        s(&r)+    );+}++#[test]+fn a_bridge_creation_waits_for_the_user_to_go_idle() {+    let m = Sim::new("presence-wait");+    let b = m.boot();+    m.foreground_other_app();+    m.last_input(5);+    let started = std::time::Instant::now();+    let r = std::thread::scope(|sc| {+        let h = sc.spawn(|| b.call("swx_new_document", json!({"type": "part", "purpose": "bridge", "waitForIdleSeconds": 20})));+        std::thread::sleep(std::time::Duration::from_millis(1500));+        m.last_input(3600);+        h.join().unwrap()+    });+    check!(+        b,+        "swx_new_document purpose:bridge WAITED while the user worked, and went ahead when they stopped",+        r["success"] == true && started.elapsed() >= std::time::Duration::from_millis(1400),+        format!("{} after {:?}", s(&r), started.elapsed())+    );+}++#[test]+fn reset_closes_the_bridges_own_scratch_while_the_user_is_present_but_not_the_active_one() {+    let m = Sim::new("presence-reset");+    let b = m.boot();+    // While the user is away: their document, then three of the bridge's.+    b.call("swx_open", json!({"path": "C:\\mock\\plate.sldprt"}));+    let mut ours = Vec::new();+    for _ in 0..3 {+        let r = b.call("swx_new_document", json!({"type": "part"}));+        ours.push(r["created"]["title"].as_str().unwrap_or("").to_string());+    }+    // The user sits down at SOLIDWORKS.+    m.foreground_solidworks(&b);+    m.last_input(5);+    let r = b.call("swx_reset_workspace", json!({}));+    let closed: Vec<String> = r["closed"].as_array().map(|a| a.iter().filter_map(|t| t.as_str().map(String::from)).collect()).unwrap_or_default();+    check!(+        b,+        "swx_reset_workspace while the user is present: closes the bridge's own scratch, leaves the ACTIVE one, says so",+        r["success"] == true && r["userPresent"] == true && closed == ours[..2].to_vec() && r["leftActive"] == json!([ours[2]]),+        s(&r)+    );+    let open = docs(&b);+    check!(+        b,+        "...the user's document and the active scratch part are still open; nothing else of ours is",+        open.iter().any(|t| t.to_lowercase().contains("plate")) && open.contains(&ours[2]) && !open.contains(&ours[0]) && !open.contains(&ours[1]),+        format!("{open:?}")+    );+    check!(+        b,+        "each close is ledgered with userPresent:true",+        ledger(&b).iter().filter(|e| e["action"] == "document_closed" && e["detail"]["userPresent"] == true).count() == 2,+        s(&json!(ledger(&b)))+    );+    let r = b.call("swx_reset_workspace", json!({}));+    check!(+        b,+        "again: only the active one is left, so no document qualifies — refused, naming it",+        r["errorCode"] == "user_is_active" && r["leftActive"] == json!([ours[2]]),+        s(&r)+    );+}++#[test]+fn the_selftest_stops_when_the_user_sits_down_and_closes_its_own_part_first() {+    let m = Sim::new("presence-selftest");+    // The user arrives once the simulated SOLIDWORKS has made one document+    // (the self-test's shared part, made for T01).+    std::fs::write(m.dir.join("user-arrives-after-documents"), "1").unwrap();+    let b = m.boot();+    let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03"], "waitForIdleSeconds": 0}));+    let names: Vec<String> = r["results"].as_array().map(|a| a.iter().filter_map(|x| x["name"].as_str().map(String::from)).collect()).unwrap_or_default();+    check!(+        b,+        "T01 ran; before T03 the user was at the machine: the run STOPPED (stoppedFor userPresent), it did not plough on",+        r["stoppedFor"] == "userPresent" && r["complete"] == false && r["stopped"]["beforeCheck"] == "T03"+            && names.iter().any(|n| n.starts_with("T01")) && !names.iter().any(|n| n.starts_with("T03"))+            && r["_hint"].as_str().unwrap_or("").contains("THE USER IS AT THE MACHINE"),+        s(&r)+    );+    check!(b, "...having closed its own shared part: nothing of the run's is left on the user's screen", docs(&b).is_empty(), format!("{:?}", docs(&b)));+}
tools/dsl-live-check.py+45−4
@@ -22,6 +22,11 @@ import argparse, json, math, os, subprocess, sys, time PI = math.pi TARGET = "adom_drew2" THREAD = "SolidWorks Bridge"+REUSE = {"path": None}   # --reuse: one bridge-made part, cleared between checks (clear_part, 0.10.6+)+# Checks that keep their own fresh part even with --reuse: they set a material or colour that+# would survive clear_part and contaminate later checks, or they assert feature names, whose+# numbering after a clear is unmeasured.+FRESH_ALWAYS = {"T15", "T16", "T22", "T24"} REL_TOL = 2e-3          # volumes: 0.2 % (SOLIDWORKS mass properties are far tighter than this) MM_TOL = 0.01           # bounding boxes: 0.01 mm @@ -269,8 +274,15 @@ def union_bbox(bodies): def run_check(cid, title, issue, script, exp):     rec = {"id": cid, "title": title, "issue": issue, "expect": exp, "fails": []}     t0 = time.time()-    r = call("swx_run_script", {"script": script, "reason": f"dsl-live-check {cid}: {title}"})+    req = {"script": script, "reason": f"dsl-live-check {cid}: {title}"}+    if REUSE["path"] and cid not in FRESH_ALWAYS and script and script[0].get("op") == "new_part":+        req["reuseDocument"] = REUSE["path"]+        rec["reused"] = True+    r = call("swx_run_script", req)     rec["runScript"] = r+    if rec.get("reused"):+        st0 = ((r.get("steps") or [{}])[0].get("result") or {})+        rec["clearPasses"] = st0.get("passes")     steps = r.get("steps") or []     doc = r.get("documentName")     if "expectError" in exp:@@ -400,6 +412,7 @@ def main():     ap.add_argument("--only", default="")     ap.add_argument("--keep", action="store_true", help="leave the scratch parts open")     ap.add_argument("--report", default="dsl-live-report.json")+    ap.add_argument("--reuse", action="store_true", help="run every check that allows it in ONE bridge-made part (clear_part between checks) instead of a fresh part each")     a = ap.parse_args()     TARGET = a.target     st = call("swx_status", {"reason": "dsl-live-check: is SOLIDWORKS ready"})@@ -412,6 +425,10 @@ def main():     only = {x.strip() for x in a.only.split(",") if x.strip()}     results = []     plan = checks()+    if a.reuse:+        nd = call("swx_new_document", {"type": "part", "reason": "dsl-live-check --reuse: the one part every check that allows it runs in"})+        REUSE["path"] = (nd.get("documentIdentity") or {}).get("path") or (nd.get("created") or {}).get("path")+        print(f"reuse part: {'created' if REUSE['path'] else 'FAILED (' + str(nd.get('errorCode')) + '), running with fresh parts'}")     for cid, title, issue, script, exp in plan:         if only and cid not in only:             continue@@ -420,8 +437,31 @@ def main():         print(f"{'PASS' if not rec['fails'] else 'FAIL'}  {cid}  {title}  ({rec.get('seconds')} s)")         for f in rec["fails"]:             print(f"        - {f}")-        if not a.keep:-            call("swx_reset_workspace", {"reason": f"dsl-live-check: close the scratch part of {cid}"})+        if not a.keep and not rec.get("reused"):+            doc = (rec.get("runScript") or {}).get("documentName")+            if REUSE["path"] and doc:+                # MEASURED 2026-10-15: swx_reset_workspace closes EVERY bridge-made unsaved part, the shared+                # reuse part included; 19 later checks then failed "clear_part needs a document". Close only+                # this check's own part.+                rw = call("swx_close_doc", {"targetDocument": doc, "save": False, "reason": f"dsl-live-check: close the fresh part of {cid} (reuse mode keeps the shared part)"})+                if not ok(rw):+                    rw = call("swx_reset_workspace", {"reason": f"dsl-live-check: close the scratch part of {cid} (close_doc refused: {rw.get('errorCode')})"})+                    nd = call("swx_new_document", {"type": "part", "reason": "dsl-live-check --reuse: re-create the shared part after a workspace reset"})+                    REUSE["path"] = (nd.get("documentIdentity") or {}).get("path") or (nd.get("created") or {}).get("path")+            else:+                rw = call("swx_reset_workspace", {"reason": f"dsl-live-check: close the scratch part of {cid}"})+            if not ok(rw):+                # MEASURED 2026-10-14: 17 closes in a row were refused user_present_refusal while the+                # user worked at the keyboard, and the battery kept creating parts: 18 windows piled up+                # on his screen, SOLIDWORKS reached 15 GB. A refused close STOPS the battery.+                print(f"STOP  the close after {cid} was refused ({rw.get('errorCode')}: {str(rw.get('error'))[:140]}); "+                      f"no more parts will be created. Re-run when the user is away from the keyboard.")+                results.append({"id": "STOP", "title": "battery stopped: a close was refused", "fails": [f"{rw.get('errorCode')}: {str(rw.get('error'))[:200]}"]})+                break+    if REUSE["path"] and not a.keep:+        rw = call("swx_reset_workspace", {"reason": "dsl-live-check --reuse: close the shared part"})+        if not ok(rw):+            print(f"WARN  the shared part could not be closed ({rw.get('errorCode')}); it is the bridge's own unsaved scratch part — swx_reset_workspace closes it once the user is idle")     if not only or "T23" in only:         rec = ownership_check()         results.append(rec)@@ -433,7 +473,8 @@ def main():     passed = sum(1 for r in results if not r["fails"])     report = {"when": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "target": TARGET,               "bridgeVersion": st.get("bridgeVersion"), "solidworks": {k: sw.get(k) for k in ("majorVersion", "edition", "versionSupport")},-              "passed": passed, "failed": len(results) - passed, "checks": results}+              "passed": passed, "failed": len(results) - passed, "reuse": bool(REUSE["path"]),+              "reusedChecks": sum(1 for r in results if r.get("reused")), "checks": results}     json.dump(report, open(a.report, "w"), indent=1)     print(f"\n{passed}/{len(results)} passed. Full evidence: {a.report}")     return 0 if passed == len(results) else 1
uninstall.sh