app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
0.10.9 source
17 files changed
+581−31
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+28−1SKILL.md+14−5bridge.json+1−1install.shpackage.json+1−1skills/solidworks-memory/SKILL.md+15src/launchos.rs+9src/presence.rs+149−11src/selftest.rs+45−2src/verbs.rs+31−1src/verbs3.rs+82−2tests/presence.rs+158tools/dsl-live-check.py+45−4uninstall.shBRIDGE_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.shpackage.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