← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.10.5+0.10.6
Cargo.lock+1−1
@@ -122,7 +122,7 @@ dependencies = [  [[package]] name = "swx-bridge"-version = "0.10.5"+version = "0.10.6" dependencies = [  "miniz_oxide",  "serde",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.5"+version = "0.10.6" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+33−3
@@ -9,7 +9,7 @@ One native binary. No Python, no PowerShell, no interpreter to provision: ab spawns `swx-bridge.exe` directly (`spawn.kind: "exe"`) and it is serving in about ten milliseconds. -## What it does — 70 verbs+## What it does — 71 verbs  | Area | Verbs | |---|---|@@ -23,7 +23,7 @@ about ten milliseconds. | Read the design back | `swx_design_info`, `swx_inspect_bodies` (geometry in mm), `swx_get_parameters`, `swx_set_parameter`, `swx_mass_properties`, `swx_bom`, `swx_interference`, `swx_rebuild_check`, `swx_document_info` | | Metadata | `swx_props_get`, `swx_props_set` (custom properties, file- or configuration-scope) | | Sign-in | `swx_sign_in` — signs in to 3DEXPERIENCE for a launch this bridge started, from the user's own local vault; the password never reaches a reply, a log or the container, and a rejected password is never retried. |-| App & documents | `swx_get_app_state`, `swx_state`, `swx_status`, `swx_readiness`, `swx_progress`, `swx_start`, `swx_stop`, `swx_kill`, `swx_activate_document`, `swx_close_all_documents`, `swx_reset_workspace`, `swx_show_part/_assembly/_drawing` |+| App & documents | `swx_get_app_state`, `swx_state`, `swx_status`, `swx_readiness`, `swx_progress`, `swx_start`, `swx_stop`, `swx_kill`, `swx_com_respawn` (restarts only the bridge's own COM host, to release every reference it holds), `swx_activate_document`, `swx_close_all_documents`, `swx_reset_workspace`, `swx_show_part/_assembly/_drawing` | | Windows & etiquette | `swx_window_info`, `swx_dismiss_blocking_dialogs`, `swx_close_window`, `swx_check_recovery`, `swx_screenshot`, `swx_set_driving_badge`, `swx_action_ledger`, `swx_verb_times` | | Preferences & cloud | `swx_get_preferences`, `swx_set_preference`, `swx_cloud_status` | | Bridge | `swx_describe`, `swx_com_eval`, `swx_uninstall`, `swx_selftest` (one JSON saying what works on THIS SOLIDWORKS install — see [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md)) |@@ -498,7 +498,37 @@ not.  ## Roadmap -- v0.10.5 (this): **memory, after a live incident.** On 2026-10-14 the+- v0.10.6 (this): **fixing the memory growth at its root.** Measured+  2026-10-14 on 0.10.5 (12 parts built and closed, on a fresh SOLIDWORKS+  2026): SOLIDWORKS's private bytes rose 520 MB, 913, 1191, then +83 MB per part+  to 2035 MB. Handles went 2845 to 4539 (about +125 per part).+  `GetDocumentCount` was back at 0 every time. Every created-and-closed document+  costs about 83 MB for good. Drew: "Throttling is a painful solution."+  - **Hidden documents.** `visible:false` on `swx_new_document` and on the+    script's `new_part` / `new_assembly`. It wraps NewDocument in+    `ISldWorks::DocumentVisible(false … true)`. That is a session setting, so it+    is put back on every path, including when the creation fails. It is retried,+    the server tries again if needed, and both are ledgered. The self-test and+    the quit ladder's scratch part are hidden; user scripts stay visible by+    default. A hidden document gets its scratch file through the document object+    itself, since it may not be the active document.+  - **One document for a battery.** A new `clear_part` op deletes every feature+    after the origin, using `DeleteSelection2` with children and absorbed+    features, and only in a part the bridge created. `swx_run_script+    {"reuseDocument": …}` turns a script's leading `new_part` into `clear_part`.+    The self-test runs every check it can in one hidden part, and reports+    `documentsCreated` and `reusedDocument` per row.+  - **`swx_com_respawn {reason}`.** It restarts only the bridge's own COM host,+    which releases every COM reference the bridge holds. It never calls+    SOLIDWORKS, refuses while a verb is in flight, and is ledgered. It settles+    whether the bridge pins anything.+  - The 0.10.5 recycle stays as the safety net.+  - `swx_show_*` now activates with `swDontRebuildActiveDoc`. It passed 0, which+    is the prompting `swUserDecision`.+  - Whether hiding or reuse stops the growth is unmeasured.++  623 tests.+- v0.10.5: **memory, after a live incident.** On 2026-10-14 the   SOLIDWORKS Resource Monitor warned that "available committed memory is   critically low": 72.3 of 76.6 GB was committed. The bridge-started SOLIDWORKS   held 10.6 GB with no documents open, after about a hundred scratch parts had
bridge.json+2−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "solidworks",   "displayName": "SOLIDWORKS",-  "version": "0.10.5",+  "version": "0.10.6",   "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",@@ -32,6 +32,7 @@     "swx_close_window",     "swx_cloud_status",     "swx_com_eval",+    "swx_com_respawn",     "swx_describe",     "swx_design_info",     "swx_dismiss_blocking_dialogs",
docs/COMPATIBILITY.md+19−2
@@ -12,9 +12,10 @@ only ever **run** against **3DEXPERIENCE SOLIDWORKS 2026** (Makers, revision ## The answer  - **[audited] Supported from SOLIDWORKS 2021.** Every COM member the bridge calls-  (149 rows below, covering `com.rs`, the modelling DSL and the renderer) was+  (165 rows below, covering `com.rs`, the modelling DSL and the renderer) was   looked up on help.solidworks.com. The newest is **SOLIDWORKS 2017 (revision-  25)**: `IFeatureManager::FeatureCut4` and `ISldWorks::GetCurrentLicenseType`.+  25)**: `IFeatureManager::FeatureCut4`, `ISldWorks::GetCurrentLicenseType` and+  `IModelDocExtension::SelectByRay` (a fallback rung the bridge skips below 2017).   Nothing is newer than 2021, so nothing needs a fallback on 2021. - **[measured] Tested on SOLIDWORKS 2026, live.** On 2026-10-14 the modelling   ops of issues #1 and #2 ran on SOLIDWORKS 2026 (major 34, 3DEXPERIENCE@@ -95,13 +96,18 @@ available in SOLIDWORKS 2021. | `ISelectionMgr::CreateSelectData` | 2004 FCS, Revision Number 12 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iselectionmgr~createselectdata.html) | | `IMathUtility::CreateTransform` | 2001 FCS, Revision Number 9.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imathutility~createtransform.html) | | `IModelDocExtension::CustomPropertyManager` | 2007 FCS, Revision Number 15.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~custompropertymanager.html) |+| `IModelDocExtension::DeleteSelection2` | 2006 SP1, Revision Number 14.1 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~deleteselection2.html) | | `IMassProperty::Density` | 2003 SP2, Revision Number 11.2 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imassproperty~density.html) | | `IModelView::DisplayMode` | 2004 SP1, Revision Number 12.1 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodelview~displaymode.html) |+| `ISldWorks::DocumentVisible` | — (no Availability line: an original-API member) | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~documentvisible.html) | | `IInterferenceDetectionMgr::Done` | 2007 FCS, Revision Number 15.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iinterferencedetectionmgr~done.html) | | `IModelDoc2::EditRebuild3` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~editrebuild3.html) | | `IEquationMgr::Equation` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iequationmgr~equation.html) |+| `ICurve::Evaluate2` | 2007 SP5, Revision Number 15.5 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.icurve~evaluate2.html) |+| `ISurface::EvaluateAtPoint` | — (no Availability line: an original-API member) | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isurface~evaluateatpoint.html) | | `ISldWorks::ExitApp` | — (no Availability line: an original-API member) | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~exitapp.html) | | `IModelDoc2::Extension` | 2003 FCS, Revision Number 11.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~extension.html) |+| `IFace2::FaceInSurfaceSense` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iface2~faceinsurfacesense.html) | | `IFeatureManager::FeatureCircularPattern4` | 2013 FCS, Revision Number 21.0 | yes | obsolete → IFeatureManager::FeatureCircularPattern5 | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeaturemanager~featurecircularpattern4.html) | | `IFeatureManager::FeatureCut4` | 2017 FCS, Revision Number 25.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeaturemanager~featurecut4.html) | | `IFeatureManager::FeatureExtrusion3` | 2014 FCS, Revision Number 22.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeaturemanager~featureextrusion3.html) |@@ -115,11 +121,15 @@ available in SOLIDWORKS 2021. | `ICustomPropertyManager::Get4` | 2011 SP4, Revision Number 19.4 | yes | obsolete → ICustomPropertyManager::Get5 | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.icustompropertymanager~get4.html) | | `IPartDoc::GetBodies2` | 2003 FCS, Revision Number 11.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ipartdoc~getbodies2.html) | | `IBody2::GetBodyBox` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ibody2~getbodybox.html) |+| `IEdge::GetClosestPointOn` | — (no Availability line: an original-API member) | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iedge~getclosestpointon.html) |+| `IFace2::GetClosestPointOn` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iface2~getclosestpointon.html) | | `IAssemblyDoc::GetComponentCount` | 2006 FCS, Revision Number 14 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iassemblydoc~getcomponentcount.html) | | `IAssemblyDoc::GetComponents` | 2006 FCS, Revision Number 14 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iassemblydoc~getcomponents.html) | | `IModelDoc2::GetConfigurationNames` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~getconfigurationnames.html) | | `IEquationMgr::GetCount` | 99, datecode 1999207 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iequationmgr~getcount.html) | | `ISldWorks::GetCurrentLicenseType` | 2017 FCS, Revision Number 25.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~getcurrentlicensetype.html) |+| `IEdge::GetCurve` | — (no Availability line: an original-API member) | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iedge~getcurve.html) |+| `IEdge::GetCurveParams2` | 2000 FCS, Revision Number 8.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iedge~getcurveparams2.html) | | `ISldWorks::GetDocumentCount` | 2009 FCS, Revision Number 17.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~getdocumentcount.html) | | `IPackAndGo::GetDocumentSaveToNames` | 2011 FCS, Revision Number 19.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ipackandgo~getdocumentsavetonames.html) | | `ISldWorks::GetDocumentTemplate` | 2001Plus SP4, Revision Number 10.4 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~getdocumenttemplate.html) |@@ -150,8 +160,11 @@ available in SOLIDWORKS 2021. | `ISldWorks::GetProcessID` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~getprocessid.html) | | `IModelDoc2::GetSaveFlag` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~getsaveflag.html) | | `ISelectionMgr::GetSelectedObject6` | 2006 FCS, Revision Number 14 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iselectionmgr~getselectedobject6.html) |+| `ISelectionMgr::GetSelectedObjectCount2` | 2006 FCS, Revision Number 14.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iselectionmgr~getselectedobjectcount2.html) | | `IComponent2::GetSuppression` | 2001Plus FCS, Revision Number 10.0 | yes | obsolete → IComponent2::GetSuppression2 | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.icomponent2~getsuppression.html) |+| `IFace2::GetSurface` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iface2~getsurface.html) | | `IModelDoc2::GetTitle` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~gettitle.html) |+| `IEdge::GetTwoAdjacentFaces2` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iedge~gettwoadjacentfaces2.html) | | `IModelDoc2::GetType` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~gettype.html) | | `IFeature::GetTypeName2` | 2008 FCS, Revision Number 16.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeature~gettypename2.html) | | `IModelDocExtension::GetUserPreferenceInteger` | 2009 FCS, Revision Number 17.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~getuserpreferenceinteger.html) |@@ -174,6 +187,7 @@ available in SOLIDWORKS 2021. | `IInterferenceDetectionMgr::MakeInterferingPartsTransparent` | 2007 FCS, Revision Number 15.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iinterferencedetectionmgr~makeinterferingpartstransparent.html) | | `ISelectData::Mark` | 2004 FCS, Revision Number 12 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iselectdata~mark.html) | | `IMassProperty::Mass` | 2003 SP2, Revision Number 11.2 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imassproperty~mass.html) |+| `IModelDocExtension::MultiSelect2` | 2012 SP04, Revision Number 20.4 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~multiselect2.html) | | `IFeature::Name` | — (no Availability line: an original-API member) | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeature~name.html) | | `IBody2::Name` | 2005 FCS, Revision Number 13 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ibody2~name.html) | | `IConfiguration::Name` | — (no Availability line: an original-API member) | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iconfiguration~name.html) |@@ -193,9 +207,11 @@ available in SOLIDWORKS 2021. | `IModelDoc2::SaveBMP` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~savebmp.html) | | `IModelDocExtension::SavePackAndGo` | 2011 FCS, Revision Number 19.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~savepackandgo.html) | | `IModelView::Scale2` | 99, datecode 1999207 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodelview~scale2.html) |+| `IFeature::Select2` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeature~select2.html) | | `IEntity::Select4` | 2004 FCS, Revision Number 12.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ientity~select4.html) | | `IModelDoc2::SelectByID` | — (no Availability line: an original-API member) | yes | obsolete → IModelDocExtension::SelectByID2 | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~selectbyid.html) | | `IModelDocExtension::SelectByID2` | 2005 FCS, Revision Number 13.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~selectbyid2.html) |+| `IModelDocExtension::SelectByRay` | 2017 FCS, Revision Number 25.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~selectbyray.html) | | `IModelDoc2::SelectionManager` | 2001Plus FCS, Revision Number 10 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~selectionmanager.html) | | `IPartDoc::SetMaterialPropertyName2` | 2005 FCS, Revision Number 13.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ipartdoc~setmaterialpropertyname2.html) | | `IPackAndGo::SetSaveToName` | 2011 FCS, Revision Number 19.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ipackandgo~setsavetoname.html) |@@ -218,6 +234,7 @@ available in SOLIDWORKS 2021. | `IModelDocExtension::ViewDisplayRealView` | 2005 FCS, Revision Number 13.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~viewdisplayrealview.html) | | `IModelDoc2::ViewZoomtofit2` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~viewzoomtofit2.html) | | `IBody2::Visible` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ibody2~visible.html) |+| `IModelDoc2::Visible` | 2001Plus FCS, Revision Number 10.0 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~visible.html) | | `IMassProperty::Volume` | 2003 SP2, Revision Number 11.2 | yes |  | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imassproperty~volume.html) |  Enum values the DSL passes are read from the published swconst pages (cited
install.sh

  
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "solidworks-bridge",-  "version": "0.10.5",+  "version": "0.10.6",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the SOLIDWORKS bridge (swx_* verbs). The bridge runtime itself is the release zip — a single native swx-bridge.exe; Adom Bridge loads that.",   "tags": [
references/modelling-and-export.md+14
@@ -40,6 +40,20 @@ adom-bridge --ai-thread "<thread>" swx_save_as '{"outputPath":"C:\\parts\\bracke  - `swx_new_document` gives you an **unsaved** document: it has a title (`Part1`)   and **no path** until `swx_save_as`. Target it by `name`, not by path.+- **Hidden documents and reuse (0.10.6).** Measured 2026-10-14: every document created+  and closed left SOLIDWORKS about 83 MB and 125 handles heavier, for the life of the+  session.+  - Make scratch and test geometry hidden with `swx_new_document {"type":"part",+    "visible":false}`, or `{"op":"new_part","visible":false}` in a script. The document+    has no window, and every object-model verb works on it. SOLIDWORKS cannot show a+    hidden-created document later, so save it and reopen it to see it.+  - A user's own script stays visible by default.+  - For a batch, run each script with `"reuseDocument": "<path of a part the bridge+    made>"`. Its leading `new_part` becomes `clear_part`, which deletes every feature+    after the origin, and only in a part the bridge created. N checks then cost one+    document.+  - Whether hiding or reuse stops the growth is UNMEASURED. See the+    **solidworks-memory** skill. - `swx_run_script` takes a JSON **array of steps**, not a string of code —   SOLIDWORKS has no in-process scripting host the way Fusion has Python. Every   dimension is in **millimetres**. A rejected script returns the full op list
skills/solidworks-memory/SKILL.md+45−4
@@ -30,13 +30,38 @@ Parent skill: **solidworks-bridge**   (test batteries, `swx_selftest {"level":"write"}`, batch exports, many `new_part`   scripts) are what fill the machine. +## The measured curve, and what causes it++- [measured] 2026-10-14 (hosted bridge 0.10.5, a freshly started SOLIDWORKS 2026): 12 times+  `new_part` plus a 20 x 20 x 10 mm box, then `swx_reset_workspace`, sampled after every+  close with `swx_status.memory` and `GetDocumentCount`. SOLIDWORKS's private bytes went+  **520 MB, 913 after part 1, 1191 after part 2, then a straight line of +83 MB per part+  to 2035 MB after 12**. Its handle count went **2845 to 4539, about +125 per part**, also+  linear. `GetDocumentCount` was **0** after every close: no hidden documents linger.+- [measured] So every scratch document created and closed costs about **83 MB and 125+  handles for good**, for the life of the session.+- [unmeasured] The leading explanation: each new VISIBLE document gets a window and its+  own graphics context and driver resources, which SOLIDWORKS does not give back on close.+  SOLIDWORKS's own help for `IModelDoc2::Visible` says hiding a document does not release+  its user-interface resources, and points to opening documents invisibly with+  `ISldWorks::DocumentVisible` instead.+- [unmeasured] The other candidate is a COM reference the bridge itself keeps. A code+  review found none. `swx_com_respawn {"reason": ...}` settles it: it restarts only the+  bridge's own COM host, releasing every reference the bridge holds, and never calls+  SOLIDWORKS. If SOLIDWORKS's handle count drops after a respawn (allow COM's roughly+  six-minute ping rundown if the old host had to be killed), the bridge was pinning+  something.+- [unmeasured] Whether hidden documents or a reused document stop the growth is not yet+  measured. Bridge 0.10.6 makes both possible; the next live run answers it.+ ## The rules  1. **Read memory before a long run, and between its steps.** `swx_status` reports a-   `memory` block (commit used, limit and available; SOLIDWORKS's own committed bytes; the-   largest holders). [unmeasured: the bridge reports it from 0.10.5; not yet read on a-   live machine] If commit-   available is low before you start, do not start: tell the user what holds the memory.+   `memory` block (commit used, limit and available; SOLIDWORKS's own committed bytes and+   handles; the largest holders). [measured: SOLIDWORKS's private bytes and handle count,+   read through the 12-part run on 2026-10-14] [unmeasured: the pressure verdict and the+   largest-holders list] If commit available is low before you start, do not start: tell+   the user what holds the memory. 2. **Recycle a session the bridge started, never one the user is working in.** When    SOLIDWORKS's own commit passes a budget (6 GB is a sensible default on a 32 GB PC) or    commit available drops under about 10 % of the limit, and the session was started by@@ -50,6 +75,9 @@ Parent skill: **solidworks-bridge**    however much it holds. Report the largest holders with their sizes and let the user    decide. Restarting a long-running Fusion or browser is the user's call. 5. **Never change the page file or SOLIDWORKS settings** to make room. Those are the user's.+   (The bridge's hidden creation flips one session setting, `DocumentVisible`, for one+   `NewDocument` call and puts it back straight away, even when the creation fails. It+   never leaves it changed, and it ledgers both the flip and the restore.) 6. **Split long runs.** A battery of more than about 30 new parts should recycle the    bridge's session between batches. [unmeasured] `swx_selftest {"level":"write"}` does    this itself from 0.10.5: before each check it reads memory, recycles a session the@@ -59,6 +87,19 @@ Parent skill: **solidworks-bridge**    critical, verbs that create or load geometry answer `low_commit_memory` and change    nothing; the reply names the holders and the safe remedy. `ignoreMemoryPressure:true`    with a reason runs the verb anyway and is ledgered: use it only when the user says so.+8. **Create scratch and test geometry hidden.** [unmeasured effect]+   `swx_new_document {"type":"part","visible":false}`, or `{"op":"new_part","visible":false}`+   in a script, creates the document with no window. Inspection, measurement, export and+   scripts all work on it. SOLIDWORKS documents that a document created hidden cannot be+   shown later: to see it, save it and reopen it. A user's own script stays visible (that+   is the default), because the user wants to see their part. `swx_selftest` and the quit+   ladder's scratch part are hidden from 0.10.6.+9. **Reuse one document for a batch.** [unmeasured effect]+   `swx_run_script {"reuseDocument": "<path of a part the bridge made>", "script": [...]}`+   runs the script in that part, and its leading `new_part` becomes `clear_part`. That+   step deletes every feature after the origin, and only in a part the bridge created. N+   checks then cost one document. `swx_selftest` does this for every check that does not+   set a material or colour or name a feature by number, and reports `documentsCreated`.  ## Diagnosing by hand (read-only) 
src/comhost/com.rs+217−19
@@ -2061,7 +2061,7 @@ impl ComBackend {             // A script that starts by creating its own document touches nothing             // that exists yet.             if let Ok(ops) = crate::script::parse(&args.get("script").cloned().unwrap_or(Value::Null)) {-                if matches!(ops.first(), Some(crate::script::Op::NewPart | crate::script::Op::NewAssembly)) {+                if matches!(ops.first(), Some(crate::script::Op::NewPart { .. } | crate::script::Op::NewAssembly { .. })) {                     return None;                 }             }@@ -2608,6 +2608,7 @@ impl Backend for ComBackend {              // v0.3             "new_document" => self.cmd_new_document(args),+            "restore_document_visible" => self.cmd_restore_document_visible(args),             "activate_document" => self.cmd_activate_document(args),             "close_all_documents" => self.cmd_close_all_documents(args),             "document_info" => self.cmd_document_info(args),@@ -2732,19 +2733,90 @@ impl ComBackend {         if !std::path::Path::new(&template).exists() {             return Err(HostError::msg(format!("template not found on disk: {template}")));         }-        let doc = sw-            .call(-                "NewDocument",-                &[&Var::bstr(&template), &Var::i4(0), &Var::r8(0.0), &Var::r8(0.0)],-            )?+        let visible = args.get("visible").and_then(|v| v.as_bool()).unwrap_or(true);+        if visible {+            let doc = Self::new_document_from(&sw, &template)?;+            return Ok(json!({"created": Self::doc_info(&doc), "template": template, "type": kind}));+        }+        // HIDDEN (0.10.6). MEASURED 2026-10-14: every created-and-closed+        // document cost SOLIDWORKS ~83 MB of commit and ~125 handles for good,+        // with GetDocumentCount back at 0 — the leading hypothesis is the+        // window and graphics context each visible document gets.+        // ISldWorks::DocumentVisible(Visible, Type) "allows the application to+        // control the display of a document in a window upon creation or+        // retrieval" (help.solidworks.com, ISldWorks::DocumentVisible), and+        // IModelDoc2::Visible's own remarks say hiding a document does NOT+        // release its user-interface resources — "open the document invisibly+        // using ISldWorks::DocumentVisible" instead. It is a SESSION setting:+        // it is put back to true immediately after NewDocument, on every path,+        // the error path included, and the reply says whether that worked.+        let b = crate::memory::hidden_bracket(+            |visible| {+                let r = sw.call("DocumentVisible", &[&Var::boolean(visible), &Var::i4(type_id)]).map(|_| ());+                if r.is_err() && visible {+                    std::thread::sleep(std::time::Duration::from_millis(300));+                }+                r+            },+            || Self::new_document_from(&sw, &template),+            3,+        );+        let mut visibility = json!({+            "requested": false,+            "hiddenWith": "ISldWorks::DocumentVisible",+            "documentVisibleSet": b.set_ok,+            "documentVisibleRestored": b.restored,+        });+        match b.made {+            Ok(doc) => {+                visibility["visibleReadBack"] = json!(doc.flag("Visible", &[]));+                // ITS FILE, BY THE OBJECT WE HOLD. A hidden document is not+                // necessarily the ACTIVE one, so the server's usual "save the+                // active document into the scratch folder" could name somebody+                // else's: the scratch file is written here, through this Disp.+                if let Some(path) = args.get("scratchPath").and_then(|v| v.as_str()).filter(|p| !p.is_empty()) {+                    let code = Self::save_as3(&doc, path, false);+                    visibility["scratchSaved"] = json!(matches!(code, Ok(0)));+                }+                Ok(json!({"created": Self::doc_info(&doc), "template": template, "type": kind, "visibility": visibility}))+            }+            Err(e) => Err(e.with_data(json!({"visibility": visibility}))),+        }+    }++    fn new_document_from(sw: &Disp, template: &str) -> Result<Disp, HostError> {+        sw.call("NewDocument", &[&Var::bstr(template), &Var::i4(0), &Var::r8(0.0), &Var::r8(0.0)])?             .as_dispatch()             .map(Disp)-            .ok_or_else(|| {-                HostError::msg(format!(-                    "SOLIDWORKS created no document from {template} (is the template valid?)"-                ))-            })?;-        Ok(json!({"created": Self::doc_info(&doc), "template": template, "type": kind}))+            .ok_or_else(|| HostError::msg(format!("SOLIDWORKS created no document from {template} (is the template valid?)")))+    }++    /// PUT THE SESSION SETTING BACK: DocumentVisible(true, type), retried —+    /// left false, every document the USER opens next would be invisible.+    fn restore_document_visible(sw: &Disp, type_id: i32) -> bool {+        for attempt in 0..3u64 {+            if sw.call("DocumentVisible", &[&Var::boolean(true), &Var::i4(type_id)]).is_ok() {+                return true;+            }+            std::thread::sleep(std::time::Duration::from_millis(200 * (attempt + 1)));+        }+        false+    }++    /// `restore_document_visible` — the server's second chance when a hidden+    /// creation reported the setting NOT restored. Only ever sets it TRUE.+    fn cmd_restore_document_visible(&mut self, args: &Value) -> Result<Value, HostError> {+        let kind = args.get("type").and_then(|v| v.as_str()).unwrap_or("part");+        let type_id = Self::type_id_from_name(kind)?;+        let sw = self.app(false, true)?;+        let restored = Self::restore_document_visible(&sw, type_id);+        if !restored {+            return Err(HostError::msg(format!(+                "ISldWorks::DocumentVisible(true, {kind}) failed three times — documents of that type \+                 may open INVISIBLE in this session until SOLIDWORKS restarts"+            )));+        }+        Ok(json!({"documentVisibleRestored": true, "type": kind}))     }      /// `swx_activate_document`. Not provable live in this session (SOLIDWORKS@@ -2759,6 +2831,13 @@ impl ComBackend {             .filter(|s| !s.is_empty())             .ok_or_else(|| HostError::msg("name (the document title, as swx_docs reports it) is required"))?;         let sw = self.app(false, true)?;+        // `visible:true` (0.10.6): a hidden document is made visible first —+        // attempted and read back (UNMEASURED; see make_visible).+        let visibility = if args.get("visible").and_then(|v| v.as_bool()) == Some(true) {+            self.resolve(&json!({"docName": name})).ok().map(|d| Self::make_visible(&d))+        } else {+            None+        };         let mut errors = 0i32;         // ActivateDoc3(Name, UseUserPreferences, Option, byref Errors).         // swRebuildOnActivation_e: 1 = swDontRebuildActiveDoc. 0 is@@ -2782,7 +2861,7 @@ impl ComBackend {                      the titles that are actually open"                 ))             })?;-        Ok(json!({"activeDocument": Self::doc_info(&doc), "errors": errors}))+        Ok(json!({"activeDocument": Self::doc_info(&doc), "errors": errors, "visibility": visibility}))     }      /// Enumerate the open documents, and NEVER return an empty list the@@ -4290,15 +4369,23 @@ impl ComBackend {     fn cmd_show(&mut self, args: &Value) -> Result<Value, HostError> {         let doc = self.resolve(args)?;         let title = doc.string("GetTitle", &[]).unwrap_or_default();+        // A HIDDEN document (0.10.6) is made visible first — attempted and read+        // back, never assumed: the help page for ISldWorks::DocumentVisible+        // says a document created invisibly "cannot" be made visible with+        // IModelDoc2::Visible (close it and reopen it instead). UNMEASURED.+        let visibility = Self::make_visible(&doc);         let mut errors = 0i32;         let activated = if args.get("activate").and_then(|v| v.as_bool()).unwrap_or(true) {             let sw = self.app(false, true)?;+            // swRebuildOnActivation_e 1 = swDontRebuildActiveDoc (0 is+            // swUserDecision, which PROMPTS — fixed here in 0.10.6 as it was in+            // activate in 0.10.2).             sw.call(                 "ActivateDoc3",                 &[                     &Var::bstr(&title),                     &Var::boolean(false),-                    &Var::i4(0),+                    &Var::i4(1),                     &Var::byref_i4(&mut errors as *mut i32),                 ],             )@@ -4313,9 +4400,31 @@ impl ComBackend {             "activated": activated,             "zoomedToFit": zoomed,             "activateErrors": errors,+            "visibility": visibility,         }))     } +    /// IModelDoc2::Visible: read; if false, put true and read again.+    fn make_visible(doc: &Disp) -> Value {+        let before = doc.flag("Visible", &[]);+        if before != Some(false) {+            return json!({"wasVisible": before, "madeVisible": false});+        }+        let put = doc.put("Visible", &Var::boolean(true)).is_ok();+        let after = doc.flag("Visible", &[]);+        json!({+            "wasVisible": false,+            "putVisible": put,+            "visibleAfter": after,+            "madeVisible": after == Some(true),+            "note": if after == Some(true) { Value::Null } else {+                json!("SOLIDWORKS kept this document hidden: a document created invisibly cannot be made \+                       visible (help.solidworks.com, ISldWorks::DocumentVisible). Save it and reopen it \+                       (swx_save_as, then swx_open) to see it.")+            },+        })+    }+     /// `swx_render_image` (0.9.18) — MEASURED 2026-10-05 on the laptop (window     /// minimized): 4K in 11.6 s, every setting read back restored, the four     /// swconst ids resolved. `shadows:true` did not appear in applied/restored —@@ -5338,6 +5447,10 @@ impl ComBackend {             let step = i + 1;             let made_before = created.as_ref().and_then(|c| c.get("created").cloned());             let detail = self.run_op(op, &pinned, &mut st).map_err(|e| {+                // The failing step's structured data (a hidden new_part's+                // `visibility`, an HRESULT) and its code travel with the error.+                let data = e.data.clone();+                let code = e.code.clone();                 let err = HostError::detailed(                     format!("step {step} ({}) failed: {}", op.name(), e.message),                     e.detail.unwrap_or_else(|| {@@ -5350,17 +5463,20 @@ impl ComBackend {                         )                     }),                 );-                match &made_before {+                let mut err = match &made_before {                     Some(doc) => err.with_created(doc.clone()),                     None => err,-                }+                };+                err.data = data;+                err.code = code;+                err             })?;             if let Some(f) = detail.get("feature").and_then(|f| f.as_str()) {                 if op.makes_feature() {                     st.last_feature = Some(f.to_string());                 }             }-            if matches!(op, Op::NewPart | Op::NewAssembly) {+            if matches!(op, Op::NewPart { .. } | Op::NewAssembly { .. }) {                 // A new document is active and starts clean; whatever the user had                 // active before is still put back only if the script moved it.                 let keep = st.restore_active.take();@@ -5371,6 +5487,7 @@ impl ComBackend {                     .and_then(|c| c.get("title"))                     .and_then(|t| t.as_str())                     .map(String::from);+                st.created_here = pinned.clone();                 created = Some(detail.clone());             }             results.push(json!({"step": step, "op": op.name(), "result": detail}));@@ -5416,8 +5533,62 @@ impl ComBackend {         use crate::script::Op;         let target = Self::doc_arg(pinned);         match op {-            Op::NewPart => self.cmd_new_document(&json!({"type": "part"})),-            Op::NewAssembly => self.cmd_new_document(&json!({"type": "assembly"})),+            Op::NewPart { visible } => self.cmd_new_document(&json!({"type": "part", "visible": visible})),+            Op::NewAssembly { visible } => self.cmd_new_document(&json!({"type": "assembly", "visible": visible})),++            Op::ClearPart => {+                // EVERY FEATURE AFTER THE ORIGIN, deleted — in a part the BRIDGE+                // created, and nowhere else: the host's own record decides,+                // whatever the server sent (0.10.6). N geometry checks then cost+                // one document instead of N.+                let doc = self.resolve(&target)?;+                let title = doc.string("GetTitle", &[]).unwrap_or_default();+                if doc.int("GetType", &[]) != Some(DOC_PART) {+                    return Err(HostError::msg(format!("clear_part works on a part; '{title}' is not one")));+                }+                if st.created_here.as_deref() != Some(title.as_str()) && !self.host_owns(&doc) {+                    return Err(HostError::coded(+                        "clear_part_not_owned",+                        format!("clear_part deletes features only in a part this bridge created; '{title}' is not one"),+                    ));+                }+                self.ensure_active(&doc, st)?;+                let after_origin = Self::features_after_origin(&doc)?;+                let names: Vec<String> = after_origin.iter().filter_map(|f| f.string("Name", &[])).collect();+                let mut deleted = 0usize;+                if !after_origin.is_empty() {+                    let _ = Self::clear_selection(&doc);+                    for f in &after_origin {+                        let _ = f.call("Select2", &[&Var::boolean(true), &Var::i4(0)]);+                    }+                    // swDeleteSelectionOptions_e: swDelete_Children 1 | swDelete_Absorbed 2+                    // (help.solidworks.com, swconst) — the sketches a feature+                    // absorbed, and anything built on it, go too. DeleteSelection2+                    // (2006 SP1) "does not ask the user to confirm the deletion".+                    let ext = doc.obj("Extension", &[])?.ok_or_else(|| HostError::msg("no ModelDocExtension"))?;+                    let ok = ext.call("DeleteSelection2", &[&Var::i4(1 | 2)])?.as_bool().unwrap_or(false);+                    let _ = Self::clear_selection(&doc);+                    let left = Self::features_after_origin(&doc)?;+                    if !ok || !left.is_empty() {+                        let still: Vec<String> = left.iter().filter_map(|f| f.string("Name", &[])).collect();+                        return Err(HostError::msg(format!(+                            "clear_part could not delete every feature (DeleteSelection2 returned {ok}; still there: {})",+                            still.join(", ")+                        )));+                    }+                    deleted = names.len();+                }+                let bodies = doc.call("GetBodies2", &[&Var::i4(0), &Var::boolean(false)]).map(|b| b.as_array().len()).unwrap_or(0);+                // The script's own memory of planes, axes and sketches is about+                // features that no longer exist.+                let keep_active = st.restore_active.take();+                let created_here = st.created_here.take();+                *st = ScriptState::default();+                st.restore_active = keep_active;+                st.created_here = created_here;+                Ok(json!({"cleared": title, "featuresDeleted": deleted, "deleted": names, "bodiesLeft": bodies,+                          "route": "DeleteSelection2(swDelete_Children | swDelete_Absorbed)"}))+            }              Op::Sketch { plane, entities, .. } => {                 let doc = self.resolve(&target)?;@@ -6081,6 +6252,31 @@ impl ComBackend {             .unwrap_or(false))     } +    /// The TOP-LEVEL features after the origin (FirstFeature / GetNextFeature;+    /// the origin is the `OriginProfileFeature`): what a script built.+    fn features_after_origin(doc: &Disp) -> Result<Vec<Disp>, HostError> {+        let mut f = doc.obj("FirstFeature", &[])?;+        let mut seen_origin = false;+        let mut out = Vec::new();+        let mut guard = 0;+        while let Some(feat) = f {+            guard += 1;+            if guard > 100_000 {+                break;+            }+            if seen_origin {+                out.push(feat.clone());+            } else if feat.string("GetTypeName2", &[]).as_deref() == Some("OriginProfileFeature") {+                seen_origin = true;+            }+            f = feat.obj("GetNextFeature", &[]).ok().flatten();+        }+        if !seen_origin {+            return Err(HostError::msg("this part has no Origin in its feature tree, so clear_part cannot tell what the script built"));+        }+        Ok(out)+    }+     /// The tree's last feature's name (FirstFeature / GetNextFeature).     fn last_feature_name(doc: &Disp) -> Option<String> {         let mut f = doc.obj("FirstFeature", &[]).ok().flatten()?;@@ -6857,6 +7053,8 @@ struct PendingSketch { /// and the document to re-activate when it ends. #[derive(Default)] struct ScriptState {+    /// The document THIS script's new_part made (clear_part may act on it).+    created_here: Option<String>,     last_feature: Option<String>,     planes: std::collections::HashMap<String, String>,     axes: std::collections::HashMap<crate::script::Axis, (String, f64)>,
src/comhost/host.rs+7
@@ -150,6 +150,11 @@ pub const READ_ONLY_COMMANDS: &[&str] = &[     // the result is the bridge's own, and the host records it as such.     "new_document",     "import",+    //   `restore_document_visible` (0.10.6) puts ISldWorks::DocumentVisible+    //                        back to TRUE for one document type — the second+    //                        chance after a hidden creation reported it was+    //                        not restored. It can only ever un-hide.+    "restore_document_visible",     //   `launch_shortcut`    STARTS SOLIDWORKS, by invoking the shortcut the     //                        3DEXPERIENCE Platform installed. A fourth     //                        exemption, and it needs its own justification@@ -837,6 +842,8 @@ pub const COMMANDS: &[&str] = &[     "eval",     // v0.3 — the Fusion-parity surface.     "new_document",+    // 0.10.6 — DocumentVisible put back after a hidden creation.+    "restore_document_visible",     // 0.9.3 — has SOLIDWORKS finished starting? (the readiness gate)     "startup_state",     "activate_document",
src/comhost/mock.rs+206−7
@@ -117,6 +117,9 @@ struct MockDoc {     /// The simulated solid model (0.10.0): features, bounding box, volume,     /// appearance — see `mock_model`.     model: Map<String, Value>,+    /// Created while ISldWorks::DocumentVisible was false for its type (0.10.6):+    /// no window. Per the help page, IModelDoc2::Visible cannot show it later.+    visible: bool, }  impl MockDoc {@@ -142,6 +145,7 @@ impl MockDoc {             "importTemp": self.import_temp,             "tempPath": self.temp_path,             "model": Value::Object(self.model.clone()),+            "visible": self.visible,         })     } @@ -181,6 +185,7 @@ impl MockDoc {             import_temp: v.get("importTemp").and_then(|x| x.as_bool()).unwrap_or(false),             temp_path: v.get("tempPath").and_then(|x| x.as_str()).map(String::from),             model: v.get("model").and_then(|x| x.as_object()).cloned().unwrap_or_default(),+            visible: v.get("visible").and_then(|x| x.as_bool()).unwrap_or(true),         })     } @@ -202,6 +207,7 @@ impl MockDoc {             import_temp: false,             temp_path: None,             model: Map::new(),+            visible: true,         }     } }@@ -275,6 +281,42 @@ fn fault(name: &str) -> bool {     crate::scratch::root().join(name).exists() } +/// ISldWorks::DocumentVisible, simulated (0.10.6): a SESSION setting per+/// document type, persisted in the data root like SOLIDWORKS keeps it for the+/// life of the process; every call is appended to `document-visible.jsonl` so a+/// test can read the order (false, NewDocument, true).+fn doc_visible_path() -> std::path::PathBuf {+    crate::scratch::root().join("mock-documentvisible.json")+}++fn doc_visible_off(kind: &str) -> bool {+    std::fs::read_to_string(doc_visible_path())+        .ok()+        .and_then(|t| serde_json::from_str::<Value>(&t).ok())+        .and_then(|v| v.get(kind).and_then(|b| b.as_bool()))+        .map(|visible| !visible)+        .unwrap_or(false)+}++fn document_visible(kind: &str, visible: bool) -> Result<(), HostError> {+    let log = crate::scratch::root().join("document-visible.jsonl");+    let mut t = std::fs::read_to_string(&log).unwrap_or_default();+    let fails = visible && (fault("mock-docvisible-restore-fails") || fault("mock-docvisible-restore-fails-always"));+    t.push_str(&json!({"visible": visible, "type": kind, "ok": !fails}).to_string());+    t.push('\n');+    let _ = std::fs::write(&log, t);+    if fails {+        return Err(HostError::msg("simulated: DocumentVisible failed"));+    }+    let mut state = std::fs::read_to_string(doc_visible_path())+        .ok()+        .and_then(|t| serde_json::from_str::<Value>(&t).ok())+        .unwrap_or_else(|| json!({}));+    state[kind] = json!(visible);+    let _ = std::fs::write(doc_visible_path(), state.to_string());+    Ok(())+}+ /// A fresh script run on the simulated install. Selection behaves as the first /// live run on SOLIDWORKS 2026 measured (the VARIANT-array MultiSelect2 selects /// nothing); `mock-select-dead-<rung>` kills a rung of the ladder too.@@ -770,6 +812,23 @@ impl MockBackend {      /// Does this host believe it created the document at `key`? Path where there     /// is one, title otherwise — the same identity rule everywhere else.+    /// What IModelDoc2::Visible = true does to a document, as the help page+    /// documents it: a document created invisibly STAYS hidden.+    fn make_visible_report(visible: bool) -> Value {+        if visible {+            return json!({"wasVisible": true, "madeVisible": false});+        }+        json!({+            "wasVisible": false,+            "putVisible": true,+            "visibleAfter": false,+            "madeVisible": false,+            "note": "SOLIDWORKS kept this document hidden: a document created invisibly cannot be made visible \+                     (help.solidworks.com, ISldWorks::DocumentVisible). Save it and reopen it (swx_save_as, then \+                     swx_open) to see it.",+        })+    }+     fn host_owns(&self, key: &str) -> bool {         if self.created_session != Some(self.session()) {             return false;@@ -799,7 +858,7 @@ impl MockBackend {                 return self.resolve(&json!({"docName": t})).ok();             }             if let Ok(ops) = crate::script::parse(&args.get("script").cloned().unwrap_or(Value::Null)) {-                if matches!(ops.first(), Some(crate::script::Op::NewPart | crate::script::Op::NewAssembly)) {+                if matches!(ops.first(), Some(crate::script::Op::NewPart { .. } | crate::script::Op::NewAssembly { .. })) {                     return None;                 }             }@@ -1047,6 +1106,15 @@ impl Backend for MockBackend {     }      fn call(&mut self, cmd: &str, args: &Value) -> Result<Value, HostError> {+        // WHAT REACHED THE SIMULATED SOLIDWORKS, when a test asks+        // (`mock-log-commands`): how a test proves a verb never called it.+        if fault("mock-log-commands") {+            let f = crate::scratch::root().join("mock-commands.log");+            let mut t = std::fs::read_to_string(&f).unwrap_or_default();+            t.push_str(cmd);+            t.push('\n');+            let _ = std::fs::write(&f, t);+        }         let out = self.dispatch(cmd, args);         // The simulated APPLICATION's session outlives the com host, because a         // real one does. Written after every command so a respawn resumes from@@ -2465,6 +2533,58 @@ impl MockBackend {             "new_document" => {                 self.running = true;                 let kind = args.get("type").and_then(|v| v.as_str()).unwrap_or("part");+                let visible = args.get("visible").and_then(|v| v.as_bool()).unwrap_or(true);+                if !visible {+                    // THE BRACKET: off, create, back on — on every path.+                    let canonical = match kind.trim().to_lowercase().as_str() {+                        "assembly" | "asm" | "sldasm" => "assembly",+                        "drawing" | "drw" | "slddrw" => "drawing",+                        _ => "part",+                    };+                    let mut inner = args.clone();+                    inner["visible"] = json!(true);+                    let b = crate::memory::hidden_bracket(+                        |visible| document_visible(canonical, visible),+                        || {+                            if fault("mock-newdoc-fails") {+                                return Err(HostError::msg(+                                    "simulated: SOLIDWORKS created no document (NewDocument failed inside the hidden bracket)",+                                ));+                            }+                            self.call("new_document", &inner)+                        },+                        3,+                    );+                    let mut visibility = json!({+                        "requested": false,+                        "hiddenWith": "ISldWorks::DocumentVisible",+                        "documentVisibleSet": b.set_ok,+                        "documentVisibleRestored": b.restored,+                    });+                    return match b.made {+                        Ok(mut r) => {+                            let key = r.pointer("/created/title").and_then(|t| t.as_str()).map(|t| format!("untitled:{t}"));+                            let read_back = key.as_deref().and_then(|k| self.find(k)).map(|d| d.visible);+                            visibility["visibleReadBack"] = json!(read_back);+                            // Its scratch file, written through the document itself.+                            if let (Some(path), Some(title)) = (+                                args.get("scratchPath").and_then(|v| v.as_str()).filter(|p| !p.is_empty()),+                                r.pointer("/created/title").and_then(|t| t.as_str()).map(String::from),+                            ) {+                                let saved = self.call("save_as", &json!({"docName": title, "out": path, "copy": false}));+                                visibility["scratchSaved"] = json!(saved.is_ok());+                                if let Ok(s) = saved {+                                    if let Some(doc) = s.get("doc").cloned() {+                                        r["created"] = doc;+                                    }+                                }+                            }+                            r["visibility"] = visibility;+                            Ok(r)+                        }+                        Err(e) => Err(e.with_data(json!({"visibility": visibility}))),+                    };+                }                 // EXHAUSTION, as the real thing reports it: SOLIDWORKS accepts                 // the call and hands back nothing. Observed live on 2026-09-03                 // for BOTH the part and the assembly template, at 17.4 GB and@@ -2498,11 +2618,41 @@ impl MockBackend {                 let mut doc = MockDoc::opened(key.clone(), doc_type, "Default".into());                 doc.title = title;                 doc.saved = false;+                // A document is created hidden whenever DocumentVisible is false+                // for its type — asked for, or LEFT false by a failed restore.+                doc.visible = !doc_visible_off(doc_type);+                // THE MEASURED GROWTH (2026-10-14): every document created costs+                // SOLIDWORKS memory it keeps after the close. The simulated+                // machine counts creations; `sldworks.json`'s+                // `growthPerDocumentMB` turns the count into private bytes.+                if let Some(d) = fake_dir() {+                    let f = d.join("documents-created");+                    let n = std::fs::read_to_string(&f).ok().and_then(|t| t.trim().parse::<u64>().ok()).unwrap_or(0);+                    let _ = std::fs::write(&f, (n + 1).to_string());+                }+                let hidden = !doc.visible;                 self.docs.push(doc);-                self.active = Some(key.clone());+                // A hidden document is not made the active one (UNMEASURED live;+                // modelled conservatively).+                if !hidden {+                    self.active = Some(key.clone());+                }                 Ok(json!({"created": self.doc_info(&key), "template": template, "type": kind}))             } +            "restore_document_visible" => {+                let kind = args.get("type").and_then(|v| v.as_str()).unwrap_or("part");+                if fault("mock-docvisible-restore-fails-always") {+                    return Err(HostError::msg(format!(+                        "ISldWorks::DocumentVisible(true, {kind}) failed three times — documents of that type may \+                         open INVISIBLE in this session until SOLIDWORKS restarts"+                    )));+                }+                let _ = std::fs::remove_file(crate::scratch::root().join("mock-docvisible-restore-fails"));+                document_visible(kind, true)?;+                Ok(json!({"documentVisibleRestored": true, "type": kind}))+            }+             "activate_document" => {                 self.require_running()?;                 let name = args@@ -2523,7 +2673,12 @@ impl MockBackend {                         ))                     })?;                 self.active = Some(key.clone());-                Ok(json!({"activeDocument": self.doc_info(&key), "errors": 0}))+                let visibility = if args.get("visible").and_then(|v| v.as_bool()) == Some(true) {+                    self.find(&key).map(|d| Self::make_visible_report(d.visible))+                } else {+                    None+                };+                Ok(json!({"activeDocument": self.doc_info(&key), "errors": 0, "visibility": visibility}))             }              "close_all_documents" => {@@ -3181,12 +3336,14 @@ impl MockBackend {                 let path = self.resolve(args)?;                 self.active = Some(path.clone());                 let info = self.doc_info(&path);+                let visibility = self.find(&path).map(|d| Self::make_visible_report(d.visible));                 Ok(json!({                     "document": info.get("title").cloned().unwrap_or(Value::Null),                     "doc": info,                     "activated": args.get("activate").and_then(|v| v.as_bool()).unwrap_or(true),                     "zoomedToFit": true,                     "activateErrors": 0,+                    "visibility": visibility,                 }))             } @@ -3728,14 +3885,54 @@ impl MockBackend {                     let target = pin_arg(&pinned);                     let detail: Result<Value, HostError> = (|| -> Result<Value, HostError> {                         Ok(match op {-                            Op::NewPart | Op::NewAssembly => {-                                let kind = if matches!(op, Op::NewPart) { "part" } else { "assembly" };-                                let r = self.call("new_document", &json!({"type": kind}))?;+                            Op::NewPart { visible } | Op::NewAssembly { visible } => {+                                let kind = if matches!(op, Op::NewPart { .. }) { "part" } else { "assembly" };+                                let r = self.call("new_document", &json!({"type": kind, "visible": visible}))?;                                 created = r.get("created").cloned().unwrap_or(Value::Null);                                 pinned = created.get("title").and_then(|t| t.as_str()).map(String::from);+                                // The real backend activates its target before a+                                // selection (ensure_active) — a HIDDEN new part is+                                // not made active by NewDocument, so model that here.+                                if !fault("mock-no-activation") {+                                    if let Ok(p) = self.resolve(&pin_arg(&pinned)) {+                                        self.active = Some(p);+                                    }+                                }                                 run = fresh_run();                                 r                             }+                            Op::ClearPart => {+                                // Only in a part the BRIDGE created — the host's own+                                // record decides (or the part this script made).+                                let path = self.resolve(&target)?;+                                let made_here = created.get("title").and_then(|t| t.as_str())+                                    .map(|t| self.find(&path).map(|d| d.title == t).unwrap_or(false))+                                    .unwrap_or(false);+                                if !made_here && !self.host_owns(&path) {+                                    let title = self.find(&path).map(|d| d.title.clone()).unwrap_or_default();+                                    return Err(HostError::coded(+                                        "clear_part_not_owned",+                                        format!("clear_part deletes features only in a part this bridge created; '{title}' is not one"),+                                    ));+                                }+                                let d = self.docs.iter_mut().find(|d| d.path == path).ok_or_else(|| HostError::msg("document vanished"))?;+                                if d.doc_type != "part" {+                                    return Err(HostError::msg(format!("clear_part works on a part; '{}' is not one", d.title)));+                                }+                                let mut model: crate::comhost::mock_model::Model =+                                    serde_json::from_value(Value::Object(d.model.clone())).unwrap_or_default();+                                let deleted: Vec<String> = model.features.iter().map(|f| f.name.clone()).collect();+                                model.clear();+                                d.bodies = model.bodies_json();+                                d.dimensions = Map::new();+                                if let Value::Object(m) = serde_json::to_value(&model).unwrap_or(Value::Null) {+                                    d.model = m;+                                }+                                d.dirty = true;+                                run = fresh_run();+                                json!({"cleared": d.title, "featuresDeleted": deleted.len(), "deleted": deleted, "bodiesLeft": 0,+                                       "route": "DeleteSelection2(swDelete_Children | swDelete_Absorbed)"})+                            }                             Op::SetProp { name, value } => {                                 let path = self.resolve(&target)?;                                 if let Some(d) = self.docs.iter_mut().find(|d| d.path == path) {@@ -3801,7 +3998,9 @@ impl MockBackend {                     match detail {                         Ok(d) => steps.push(json!({"step": i + 1, "op": op.name(), "result": d})),                         Err(e) => {-                            let err = HostError::msg(format!("step {} ({}) failed: {}", i + 1, op.name(), e.message));+                            let mut err = HostError::msg(format!("step {} ({}) failed: {}", i + 1, op.name(), e.message));+                            err.data = e.data.clone();+                            err.code = e.code.clone();                             // A part made by this script's new_part before the                             // failing step still exists, and is the bridge's.                             failure = Some(if created.is_object() { err.with_created(created.clone()) } else { err });
src/comhost/mock_model.rs+10
@@ -779,6 +779,16 @@ impl Model {     }      /// The bodies, in swx_inspect_bodies' shape.+    /// clear_part (0.10.6): every feature and body goes; the part-level colour+    /// and material stay (they are not features), and so do the name counters+    /// (whether SOLIDWORKS restarts its feature numbering is UNMEASURED, so the+    /// simulator assumes it does not).+    pub fn clear(&mut self) {+        self.bodies.clear();+        self.features.clear();+        self.face_appearances.clear();+    }+     pub fn bodies_json(&self) -> Vec<Value> {         self.bodies             .iter()
src/comhost/mod.rs+44
@@ -314,6 +314,40 @@ impl ComHost {         }     } +    /// RESPAWN THE CHILD, AND NOTHING ELSE (swx_com_respawn, 0.10.6): the+    /// child exits through its own loop (`__exit__`), so its backend is DROPPED+    /// and every COM reference it held is released by its own Release calls —+    /// not left to the ~6-minute COM ping rundown a killed process leaves.+    /// SOLIDWORKS is never called. A verb holding the host refuses (`Err`):+    /// pulling the host out from under a COM call is a different act.+    pub fn respawn(&self) -> Result<RespawnReport, String> {+        use std::sync::TryLockError;+        let mut guard = match self.live.try_lock() {+            Ok(g) => g,+            Err(TryLockError::WouldBlock) => return Err("a verb is in flight on the com host".into()),+            Err(TryLockError::Poisoned(g)) => g.into_inner(),+        };+        let old_pid = guard.as_ref().map(|l| l.pid);+        let mut graceful = old_pid.is_none();+        if let Some(live) = guard.as_mut() {+            let _ = live.stdin.write_all(b"{\"id\":0,\"cmd\":\"__exit__\"}\n");+            let _ = live.stdin.flush();+            let deadline = Instant::now() + Duration::from_secs(5);+            while Instant::now() < deadline {+                if matches!(live.child.try_wait(), Ok(Some(_))) {+                    graceful = true;+                    break;+                }+                std::thread::sleep(Duration::from_millis(50));+            }+        }+        kill(guard.take());+        self.sync_pid(&guard);+        *guard = Some(spawn_child().map_err(|e| e.message)?);+        self.sync_pid(&guard);+        Ok(RespawnReport { old_pid, new_pid: guard.as_ref().map(|l| l.pid), graceful_exit: graceful })+    }+     /// Ask the child to exit, then make sure of it. Idempotent, and it must NOT     /// block on the request lock — ab's uninstall cascade has a 120s budget, but     /// a 580s Pack and Go could be holding that lock.@@ -345,6 +379,16 @@ impl ComHost {     } } +/// What a respawn did.+#[derive(Clone, Debug)]+pub struct RespawnReport {+    pub old_pid: Option<u32>,+    pub new_pid: Option<u32>,+    /// The old child left through its own loop (references released by Drop)+    /// rather than being killed.+    pub graceful_exit: bool,+}+ /// OS-level kill of the child by pid, used only when the request lock is held /// by a long COM call and we cannot reach the `Child` handle behind it. fn kill_by_pid(pid: u32) {
src/compat.rs+12−3
@@ -37,6 +37,8 @@ pub fn major_of_year(year: u32) -> u32 { pub const GATED: &[(&str, u32)] = &[     ("IFeatureManager::FeatureCut4", 25),     ("ISldWorks::GetCurrentLicenseType", 25),+    // The selection ladder's third rung (0.10.3) — skipped below 2017.+    ("IModelDocExtension::SelectByRay", 25),     ("IFeatureManager::FeatureFillet3", 23),     ("IFeatureManager::FeatureLinearPattern4", 23),     ("ISldWorks::StartupProcessCompleted", 23),@@ -120,6 +122,9 @@ pub struct OpFact { pub const OP_FACTS: &[OpFact] = &[     OpFact { id: "new_part", what: "a new part document", checks: "every check", measured: M2026 },     OpFact { id: "new_assembly", what: "a new assembly document", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "new_part.invisible", what: "a new part created HIDDEN (DocumentVisible false around NewDocument)", checks: "not yet run live (0.10.6)", measured: UNMEASURED },+    OpFact { id: "new_assembly.invisible", what: "a new assembly created HIDDEN", checks: "not yet run live (0.10.6)", measured: UNMEASURED },+    OpFact { id: "clear_part", what: "every feature after the origin deleted from the bridge's own part (DeleteSelection2)", checks: "not yet run live (0.10.6)", measured: UNMEASURED },     OpFact { id: "sketch.plane.front", what: "a sketch on Front Plane", checks: "T01 T03 T10 T14 T27", measured: M2026 },     OpFact { id: "sketch.plane.top", what: "a sketch on Top Plane", checks: "T02 T13 T24 T30", measured: M2026 },     OpFact { id: "sketch.plane.right", what: "a sketch on Right Plane (sketch +x = model −Z, +y = model +Y; a boss goes +X)", checks: "T35", measured: M2026B },@@ -246,8 +251,11 @@ pub fn op_variants(op: &crate::script::Op, result: &serde_json::Value) -> Vec<St     use crate::script::{AppearanceTarget, Direction, EdgeSel, Op, PlaneRef};     let mut v: Vec<String> = Vec::new();     match op {-        Op::NewPart => v.push("new_part".into()),-        Op::NewAssembly => v.push("new_assembly".into()),+        Op::NewPart { visible: true } => v.push("new_part".into()),+        Op::NewPart { visible: false } => v.push("new_part.invisible".into()),+        Op::NewAssembly { visible: true } => v.push("new_assembly".into()),+        Op::NewAssembly { visible: false } => v.push("new_assembly.invisible".into()),+        Op::ClearPart => v.push("clear_part".into()),         Op::Sketch { plane, entities, .. } => {             v.push(match plane {                 PlaneRef::Named(n) => format!("sketch.plane.{}", plane_key(n)),@@ -513,7 +521,8 @@ mod tests {         assert_eq!(             unmeasured,             [-                "chamfer.allOfFeature", "extrude.both.throughAll", "fillet.createDefinition", "new_assembly", "rebuild",+                "chamfer.allOfFeature", "clear_part", "extrude.both.throughAll", "fillet.createDefinition", "new_assembly",+                "new_assembly.invisible", "new_part.invisible", "rebuild",                 "save_as", "select.entitySelect4", "select.selectByIdAtPoint", "select.selectByRay", "set_appearance.body",                 "sketch.face.front.positive", "sketch.face.right.negative", "sketch.offset.front.negative",                 "sketch.offset.named", "sketch.offset.right.negative", "sketch.offset.right.positive", "sketch.plane.named",
src/launchos.rs+8−1
@@ -1507,8 +1507,15 @@ impl LaunchOs for FakeOs {      fn memory_of(&self, _pid: u32) -> (Option<u64>, Option<u32>) {         let s = self.read_json("sldworks.json");+        // `growthPerDocumentMB` (0.10.6): the measured leak — every document+        // the simulated SOLIDWORKS created keeps that much after its close.+        let created = std::fs::read_to_string(self.dir.join("documents-created"))+            .ok()+            .and_then(|t| t.trim().parse::<u64>().ok())+            .unwrap_or(0);+        let growth = s.get("growthPerDocumentMB").and_then(|v| v.as_u64()).unwrap_or(0) * created;         (-            s.get("privateMB").and_then(|v| v.as_u64()).or(Some(800)),+            s.get("privateMB").and_then(|v| v.as_u64()).or(Some(800)).map(|m| m + growth),             s.get("handles").and_then(|v| v.as_u64()).map(|h| h as u32).or(Some(3000)),         )     }
src/memory.rs+91
@@ -381,6 +381,44 @@ pub fn log_suffix() -> String {     } } +// ── hidden documents (0.10.6) ───────────────────────────────────────────────++/// What the hidden-creation bracket did.+#[derive(Debug)]+pub struct Bracket<T, E> {+    pub made: Result<T, E>,+    /// DocumentVisible(false) succeeded (if not, nothing was created).+    pub set_ok: bool,+    /// DocumentVisible(true) succeeded afterwards.+    pub restored: bool,+}++/// THE HIDDEN-CREATION BRACKET, shared by the COM host and the simulator:+/// `set_visible(false)`, then `make` (only if that worked), then+/// `set_visible(true)` — ALWAYS, whatever `make` did, retried up to `tries`+/// times. ISldWorks::DocumentVisible is a SESSION setting: left false, every+/// document of that type the user opens next would be invisible.+pub fn hidden_bracket<T, E>(+    mut set_visible: impl FnMut(bool) -> Result<(), E>,+    make: impl FnOnce() -> Result<T, E>,+    tries: usize,+) -> Bracket<T, E> {+    let off = set_visible(false);+    let set_ok = off.is_ok();+    let made = match off {+        Ok(()) => make(),+        Err(e) => Err(e),+    };+    let mut restored = false;+    for _ in 0..tries.max(1) {+        if set_visible(true).is_ok() {+            restored = true;+            break;+        }+    }+    Bracket { made, set_ok, restored }+}+ // ── the guard ───────────────────────────────────────────────────────────────  /// Verbs that create or load geometry.@@ -723,6 +761,59 @@ mod tests {         assert!(matches!(between_checks(Pressure::Unknown, Some(10), b, None, None), Between::Continue), "nothing to act on");     } +    #[test]+    fn the_hidden_bracket_always_puts_the_setting_back() {+        use std::cell::RefCell;+        let calls = RefCell::new(Vec::new());+        // Creation fails: the setting is still restored.+        let b = hidden_bracket(+            |v| {+                calls.borrow_mut().push(v);+                Ok::<(), String>(())+            },+            || Err::<u32, String>("NewDocument failed".into()),+            3,+        );+        assert!(b.made.is_err() && b.set_ok && b.restored);+        assert_eq!(*calls.borrow(), vec![false, true], "off, then on — after a failure too");+        // The restore fails twice, then works: retried, and reported restored.+        let n = RefCell::new(0);+        let b = hidden_bracket(+            |v| {+                if v {+                    *n.borrow_mut() += 1;+                    if *n.borrow() < 3 {+                        return Err("busy".to_string());+                    }+                }+                Ok(())+            },+            || Ok::<u32, String>(7),+            3,+        );+        assert_eq!((b.made, b.restored, *n.borrow()), (Ok(7), true, 3));+        // It never works: reported NOT restored, after every try.+        let n = RefCell::new(0);+        let b = hidden_bracket(|v| if v { *n.borrow_mut() += 1; Err("no".to_string()) } else { Ok(()) }, || Ok::<u32, String>(1), 3);+        assert!(!b.restored && *n.borrow() == 3);+        // DocumentVisible(false) fails: nothing is created — and it is still set back.+        let made = RefCell::new(false);+        let calls = RefCell::new(Vec::new());+        let b = hidden_bracket(+            |v| {+                calls.borrow_mut().push(v);+                if v { Ok(()) } else { Err("refused".to_string()) }+            },+            || {+                *made.borrow_mut() = true;+                Ok::<u32, String>(1)+            },+            3,+        );+        assert!(!b.set_ok && b.made.is_err() && !*made.borrow() && b.restored);+        assert_eq!(*calls.borrow(), vec![false, true]);+    }+     #[test]     fn the_report_and_its_hint_name_the_holders() {         let s = Sample {
src/script.rs+33−8
@@ -171,10 +171,16 @@ pub enum AppearanceTarget {  #[derive(Debug, Clone, PartialEq)] pub enum Op {-    /// Create a new part document from the resolved part template.-    NewPart,+    /// Create a new part document from the resolved part template. `visible`+    /// false creates it HIDDEN (ISldWorks::DocumentVisible bracketed around+    /// NewDocument, 0.10.6): no window, no graphics context.+    NewPart { visible: bool },     /// Create a new assembly document from the resolved assembly template.-    NewAssembly,+    NewAssembly { visible: bool },+    /// DELETE EVERY FEATURE after the origin of a part the BRIDGE created+    /// (0.10.6): N geometry checks cost one document instead of N. Refused on+    /// any document the bridge did not create.+    ClearPart,     /// ONE sketch holding every entity listed (0.10.0) — several closed     /// profiles in one sketch extrude into rings, walls or hole patterns in ONE     /// feature. `shorthand` is the op the caller wrote: "sketch", "sketch_rect"@@ -206,8 +212,9 @@ impl Op {     /// The slug used in the DSL and echoed in the per-step result.     pub fn name(&self) -> &'static str {         match self {-            Op::NewPart => "new_part",-            Op::NewAssembly => "new_assembly",+            Op::NewPart { .. } => "new_part",+            Op::NewAssembly { .. } => "new_assembly",+            Op::ClearPart => "clear_part",             Op::Sketch { shorthand, .. } => shorthand,             Op::Extrude(_) => "extrude",             Op::Cut(_) => "cut",@@ -239,6 +246,7 @@ impl Op { pub const OPS: &[&str] = &[     "new_part",     "new_assembly",+    "clear_part",     "sketch",     "sketch_rect",     "sketch_circle",@@ -673,6 +681,16 @@ fn count(v: &Value, key: &str) -> Result<u32, String> {     Ok(n as u32) } +/// `visible` on new_part / new_assembly: a boolean, default true (a user's+/// script shows its part).+fn visible_arg(step: &Value) -> Result<bool, String> {+    match step.get("visible") {+        None | Some(Value::Null) => Ok(true),+        Some(Value::Bool(b)) => Ok(*b),+        Some(other) => Err(format!("visible must be true or false, got {other}")),+    }+}+ fn feature_ref(step: &Value) -> String {     step.get("feature").and_then(|v| v.as_str()).filter(|s| !s.trim().is_empty()).unwrap_or("last").to_string() }@@ -683,8 +701,9 @@ fn parse_step(step: &Value) -> Result<Op, String> {         .and_then(|v| v.as_str())         .ok_or_else(|| format!("missing `op` (one of: {})", OPS.join(", ")))?;     Ok(match op {-        "new_part" => Op::NewPart,-        "new_assembly" => Op::NewAssembly,+        "new_part" => Op::NewPart { visible: visible_arg(step)? },+        "new_assembly" => Op::NewAssembly { visible: visible_arg(step)? },+        "clear_part" => Op::ClearPart,         "sketch_rect" => Op::Sketch { shorthand: "sketch_rect", plane: plane_ref(step)?, entities: vec![rect(step)?] },         "sketch_circle" => Op::Sketch { shorthand: "sketch_circle", plane: plane_ref(step)?, entities: vec![circle(step)?] },         "sketch" => {@@ -811,11 +830,17 @@ pub fn validate(ops: &[Op], has_open_doc: bool) -> Result<(), String> {     for (i, op) in ops.iter().enumerate() {         let at = i + 1;         match op {-            Op::NewPart | Op::NewAssembly => {+            Op::NewPart { .. } | Op::NewAssembly { .. } => {                 doc = true;                 have_feature = false;                 continue;             }+            Op::ClearPart if doc => {+                // Every feature is gone: nothing to call "last", no sketch pending.+                have_feature = false;+                sketch_pending = false;+                continue;+            }             _ if !doc => {                 return Err(format!(                     "step {at} ({}) needs a document: start the script with new_part, or open one \
src/selftest.rs+101−7
@@ -179,23 +179,47 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {         let memory_override = crate::memory::override_reason(args).ok().flatten();         let mut memory_log = MemoryLog::default();         let mut last_check: Option<&'static str> = None;+        // ONE HIDDEN DOCUMENT FOR THE BATTERY (0.10.6). Measured 2026-10-14:+        // each created-and-closed document cost SOLIDWORKS ~83 MB and ~125+        // handles for good. Every check that can runs in ONE hidden part the+        // bridge made, cleared between checks (clear_part); a check that needs+        // a fresh part (a material, a colour, a feature named by its number)+        // gets its own — hidden too.+        let mut shared: Option<String> = None;         for check in battery() {             if !only.is_empty() && !only.iter().any(|o| o == check.id) {                 continue;             }-            if let Err(stop) = memory_between(ctx, last_check, check.id, memory_override.as_deref(), &mut memory_log) {+            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;             }-            geometry_check(ctx, &mut run, &check, memory_override.as_deref());+            let reuse = reusable(&check);+            if reuse && shared.is_none() {+                shared = make_shared_document(ctx, memory_override.as_deref());+                if shared.is_some() {+                    memory_log.documents_created += 1;+                }+            }+            let target = if reuse { shared.clone() } else { None };+            if target.is_none() {+                memory_log.documents_created += 1;+            }+            let clear_failed = geometry_check(ctx, &mut run, &check, memory_override.as_deref(), target.as_deref());             let after = crate::memory::sample(crate::launchos::os().as_ref());             if let Some(last) = run.results.last_mut() {                 last["swPrivateMbAfter"] = json!(after.sw_private_mb);+                last["reusedDocument"] = json!(target.is_some());+            }+            if clear_failed {+                // A shared part that could not be cleared is replaced, not reused.+                close_shared(ctx, &mut shared);             }             last_check = Some(check.id);         }+        close_shared(ctx, &mut shared);         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) {+            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);             }         }@@ -207,7 +231,8 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {         }          // THE VERBS on one scratch part made by swx_new_document.-        let mut new_doc = json!({"type": "part"});+        let mut new_doc = json!({"type": "part", "visible": false});+        memory_log.documents_created += 1;         if let Some(r) = &memory_override {             new_doc["ignoreMemoryPressure"] = json!(true);             new_doc["reason"] = json!(r);@@ -268,6 +293,8 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { /// What the memory checks between the geometry checks did. #[derive(Default)] struct MemoryLog {+    /// Documents the run created (the shared part counts once).+    documents_created: usize,     recycled: Vec<Value>,     /// Checks run on under `ignoreMemoryPressure` where the run would have stopped.     overridden: Vec<Value>,@@ -278,6 +305,7 @@ impl MemoryLog {     fn to_json(&self, over: Option<&str>) -> Value {         let mut v = json!({             "recycled": self.recycled,+            "documentsCreated": self.documents_created,             "complete": self.stopped.is_none(),             "memory": crate::memory::report(crate::launchos::os().as_ref()),         });@@ -294,7 +322,14 @@ impl MemoryLog {  /// 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(ctx: &Ctx, last: Option<&str>, next: &str, over: Option<&str>, log: &mut MemoryLog) -> Result<(), Value> {+fn memory_between(+    ctx: &Ctx,+    last: Option<&str>,+    next: &str,+    over: Option<&str>,+    log: &mut MemoryLog,+    shared: &mut Option<String>,+) -> Result<(), Value> {     use crate::memory::Between;     let os = crate::launchos::os();     let s = crate::memory::sample(os.as_ref());@@ -302,6 +337,9 @@ fn memory_between(ctx: &Ctx, last: Option<&str>, next: &str, over: Option<&str>,     if !crate::memory::needs_attention(p, s.sw_private_mb, s.budget_mb) {         return Ok(());     }+    // The run's own shared part is closed first: it is the bridge's, and a+    // session can only be recycled with nothing open.+    close_shared(ctx, shared);     let facts = crate::memory::session_facts(ctx, os.as_ref());     match crate::memory::between_checks(p, s.sw_private_mb, s.budget_mb, facts.bridge_started, facts.open_docs) {         Between::Continue => Ok(()),@@ -326,6 +364,48 @@ fn memory_between(ctx: &Ctx, last: Option<&str>, next: &str, over: Option<&str>,     } } +/// Can this check run in the SHARED, cleared document? Not if it sets a+/// material, colour or property (they outlive clear_part), names a feature by+/// its number (numbering continues in a reused part), or checks the feature+/// list — those get a fresh, hidden part of their own.+fn reusable(c: &Check) -> bool {+    let Some(steps) = c.script.as_array() else { return false };+    if steps.first().and_then(|s| s.get("op")).and_then(|o| o.as_str()) != Some("new_part") || c.expect.features.is_some() {+        return false;+    }+    steps.iter().skip(1).all(|st| {+        let op = st.get("op").and_then(|o| o.as_str()).unwrap_or("");+        if matches!(op, "set_material" | "set_appearance" | "set_prop" | "new_part" | "new_assembly" | "save_as") {+            return false;+        }+        let named = |v: Option<&Value>| v.and_then(|f| f.as_str()).map(|f| f != "last").unwrap_or(false);+        !(named(st.get("feature")) || named(st.pointer("/edges/allOfFeature")))+    })+}++/// The battery's shared part: hidden, the bridge's own.+fn make_shared_document(ctx: &Ctx, over: Option<&str>) -> Option<String> {+    let mut a = json!({"type": "part", "visible": false});+    if let Some(r) = over {+        a["ignoreMemoryPressure"] = json!(true);+        a["reason"] = json!(r);+    }+    let made = crate::verbs::dispatch(ctx, "swx_new_document", &a);+    made.pointer("/documentIdentity/path")+        .or_else(|| made.pointer("/created/path"))+        .and_then(|p| p.as_str())+        .filter(|p| !p.is_empty())+        .or_else(|| made.pointer("/created/title").and_then(|t| t.as_str()))+        .map(String::from)+}++fn close_shared(ctx: &Ctx, shared: &mut Option<String>) {+    if let Some(t) = shared.take() {+        let key = if t.contains('\\') || t.contains('/') { "path" } else { "targetDocument" };+        let _ = crate::verbs::dispatch(ctx, "swx_close_doc", &json!({key: t, "save": false}));+    }+}+ fn compact(v: &Value) -> Value {     json!({         "success": v.get("success"),@@ -703,10 +783,23 @@ fn titles(ctx: &Ctx) -> Vec<String> { }  /// One check: build, MEASURE, compare, close what it made.-fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<&str>) {+fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<&str>, reuse: Option<&str>) -> bool {     let t = Instant::now();     let before = titles(ctx);-    let mut sargs = json!({"script": c.script});+    let mut sargs = match reuse {+        // In the shared part: its new_part becomes clear_part.+        Some(doc) => json!({"script": c.script, "reuseDocument": doc}),+        // A part of its own, HIDDEN: the self-test makes it for itself.+        None => {+            let mut script = c.script.clone();+            if let Some(first) = script.as_array_mut().and_then(|a| a.first_mut()) {+                if first.get("op").and_then(|o| o.as_str()) == Some("new_part") {+                    first["visible"] = json!(false);+                }+            }+            json!({"script": script})+        }+    };     if let Some(r) = memory_override {         sargs["ignoreMemoryPressure"] = json!(true);         sargs["reason"] = json!(r);@@ -877,4 +970,5 @@ fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<&         "measured": measured,         "fails": fails,     }));+    reuse.is_some() && r.get("error").and_then(|e| e.as_str()).map(|e| e.contains("(clear_part) failed")).unwrap_or(false) }
src/verbs.rs+65−1
@@ -1316,6 +1316,65 @@ fn stop_refusal_hint(code: &str) -> &'static str { /// rung, success measured by a PROCESS CENSUS rather than a COM call, and nothing /// disowned until the process is verifiably gone. Never a kill. Never answers a /// dialog. See `crate::quit` for the three laws and the refcount reasoning.+/// `swx_com_respawn` (0.10.6) — THE REFERENCE QUESTION, SETTLED BY MEASURING.+///+/// Measured 2026-10-14: each scratch document created and closed left+/// SOLIDWORKS ~83 MB and ~125 handles heavier, with GetDocumentCount back at+/// 0. If the BRIDGE pins something (a COM reference its host never released),+/// respawning the host releases it and the handle count drops; if not, nothing+/// changes. This respawns ONLY the bridge's own com host child — never+/// SOLIDWORKS, which it does not call at all — and refuses while a verb holds+/// the host. Ledgered.+fn swx_com_respawn(ctx: &Ctx, args: &Value) -> Value {+    let Some(reason) = arg_str(args, "reason").map(str::trim).filter(|r| r.chars().count() >= 8) else {+        return err_code(+            "reason_required",+            "swx_com_respawn needs reason:\"why\" (8+ characters)",+            "It restarts the bridge's own COM host process; the reason goes into the ledger beside the \+             old and new host pids. SOLIDWORKS is not touched.",+        );+    };+    let snap = ctx.com.snapshot();+    if snap.busy {+        return err_code(+            "com_host_busy",+            format!(+                "a verb is in flight on the com host ({}); swx_com_respawn never pulls the host out from under a COM call",+                snap.in_flight.unwrap_or_else(|| "unknown".into())+            ),+            "Wait for it to finish (swx_progress shows it), then call again. Nothing was changed.",+        );+    }+    match ctx.com.respawn() {+        Err(why) => err_code(+            "com_host_busy",+            format!("the com host could not be respawned: {why}"),+            "Nothing was changed. swx_progress shows what is in flight.",+        ),+        Ok(rep) => {+            crate::ledger::record(+                "com_host_respawn",+                json!({"by": "swx_com_respawn", "reason": reason, "oldHostPid": rep.old_pid,+                       "newHostPid": rep.new_pid, "gracefulExit": rep.graceful_exit}),+            );+            ok(json!({+                "respawned": true,+                "oldHostPid": rep.old_pid,+                "newHostPid": rep.new_pid,+                "gracefulExit": rep.graceful_exit,+                "solidworksTouched": false,+                "windowEtiquette": "background",+                "_hint": "The bridge's COM host was replaced. The old one left through its own loop, so its \+                          backend was dropped and every COM reference it held was released by Release() — \+                          not left to COM's ~6-minute ping rundown (gracefulExit:false means it had to be \+                          killed, and then the rundown applies). SOLIDWORKS was not called: the new host \+                          attaches on the next verb. Compare SOLIDWORKS's handle count (swx_status.memory) \+                          before and after: a drop means the bridge was pinning something.",+            }))+        }+    }+}+ fn swx_stop(ctx: &Ctx, args: &Value) -> Value {     gate!(ctx);     use crate::quit;@@ -1943,7 +2002,9 @@ fn prime_session(         Ok(_) => return Err("a dialog is up".into()),         Err(e) => return Err(format!("the window census could not be read: {e}")),     }-    let made = crate::verbs3::new_document(ctx, &json!({"type": "part"}));+    // HIDDEN (0.10.6): a document the bridge makes purely for itself gets no+    // window — each created-and-closed one cost SOLIDWORKS ~83 MB (measured).+    let made = crate::verbs3::new_document(ctx, &json!({"type": "part", "visible": false}));     if made.get("success").and_then(|v| v.as_bool()) != Some(true) {         return Err(format!("the scratch part could not be created: {}", made.get("error").cloned().unwrap_or(Value::Null)));     }@@ -3060,6 +3121,7 @@ pub const CATALOG: &[(&str, &str, &str)] = &[     ("swx_sign_in", "exec", "sign in to 3DEXPERIENCE for a launch this bridge started (or, with userAuthorized:true, any 3DEXPERIENCE ID login window, e.g. Visualize's or the platform updater's, recognized by its content; window:visualize|solidworks|desktopUpdate|<hwnd> picks one, and a named hwnd is probed by content, never refused on census grounds), from the user's local vault (the password never leaves the laptop or reaches a reply); the page is prepared first (user name, Continue), and ab's policy (sign_in_vault_policy, measured) currently keeps the password for the user to type; fails closed and never retries a rejected password"),     ("swx_user_presence", "read", "is the user working in this SOLIDWORKS? foreground + last input + documents the bridge did not open (the guard every quit and sweep passes)"),     ("swx_kill", "process", "force-kill SOLIDWORKS (reason + confirm; discards unsaved work)"),+    ("swx_com_respawn", "process", "respawn ONLY the bridge's own COM host child (reason): every COM reference the bridge holds is released; SOLIDWORKS is never called; refused while a verb is in flight"),     ("swx_window_info", "read", "SOLIDWORKS' windows and its main hwnd (read-only, nothing raised)"),     ("swx_dismiss_blocking_dialogs", "exec", "census of blocking SOLIDWORKS dialogs; close:true is opt-in, gated, and never touches an unattributed window"),     ("swx_close_window", "exec", "close one window by hwnd (reason + confirm)"),@@ -3209,6 +3271,7 @@ pub const STARTUP_EXEMPT: &[(&str, &str)] = &[     ("swx_sign_in", "acts on the LAUNCHER's sign-in window, never through COM — it is how a start gets past the sign-in at all"),     ("swx_user_presence", "a read about the USER, needed most during a start; asks COM for documents only on a verified-ready session"),     ("swx_selftest", "never reaches the com host itself: every check it runs goes through this same gate, and a check held at it is REPORTED — a self-test that refused to run while SOLIDWORKS starts could not say so"),+    ("swx_com_respawn", "respawns the bridge's own com host child and never calls SOLIDWORKS — the readiness gate would"), ];  /// swx_stop is gated with one carve-out: under `userAuthorized`, a plan that@@ -3400,6 +3463,7 @@ fn dispatch_after_gate(ctx: &Ctx, command: &str, args: &Value) -> Value {         "swx_progress" => swx_progress,         "swx_start" => swx_start,         "swx_stop" => swx_stop,+        "swx_com_respawn" => swx_com_respawn,         // DREW'S STANDING AUTHORIZATION, 2026-09-11: the bridge closes documents         // and quits by itself, having first written every document to disk.         "swx_close_all_safely" => crate::verbs3::close_all_safely,
src/verbs3.rs+163−11
@@ -3778,11 +3778,65 @@ fn sanitise_stem(stem: &str) -> String {     if trimmed.is_empty() { "doc".into() } else { trimmed } } +/// AFTER A HIDDEN CREATION: ISldWorks::DocumentVisible is a SESSION setting.+/// The host puts it back itself (three tries); if it reports it could not, the+/// server asks once more, and either way the outcome is ledgered — left false,+/// every document of that type the USER opens next would be invisible.+pub(crate) fn settle_document_visible(ctx: &Ctx, kind: &str, visibility: &Value) -> Value {+    let mut v = visibility.clone();+    if visibility.get("requested").and_then(|r| r.as_bool()) != Some(false) {+        return v;+    }+    let restored = visibility.get("documentVisibleRestored").and_then(|r| r.as_bool()) == Some(true);+    if restored {+        crate::ledger::record("document_visible_toggled", json!({"type": kind, "restored": true}));+        return v;+    }+    let retry = ctx.call("restore_document_visible", json!({"type": kind}), 30);+    let ok = retry.is_ok();+    v["serverRetry"] = json!({"restored": ok, "error": retry.err().map(|e| e.message)});+    v["documentVisibleRestored"] = json!(ok);+    crate::ledger::record(+        if ok { "document_visible_restored_on_retry" } else { "document_visible_restore_failed" },+        json!({"type": kind, "why": "ISldWorks::DocumentVisible is a session setting; left false, the user's next documents of this type open invisible"}),+    );+    if !ok {+        v["warning"] = json!(format!(+            "SOLIDWORKS's DocumentVisible setting for {kind} documents could NOT be put back: documents of that \+             type may open INVISIBLE in this session until SOLIDWORKS restarts. Tell the user."+        ));+    }+    v+}+ pub fn new_document(ctx: &Ctx, args: &Value) -> Value {     gate!(ctx);     let kind = arg_str(args, "type").unwrap_or("part").to_string();+    // HIDDEN DOCUMENTS (0.10.6): `visible:false` creates it with no window+    // (ISldWorks::DocumentVisible around NewDocument). The default stays+    // visible — a user's document is one they want to see.+    let visible = match args.get("visible") {+        None | Some(Value::Null) => true,+        Some(Value::Bool(b)) => *b,+        Some(other) => {+            return err_code("bad_argument", format!("visible must be true or false, got {other}"), "Omit it for a visible document.")+        }+    };     let job = ctx.jobs.start("new_document", format!("Creating a new {kind}"), 15);-    let call_args = json!({"type": kind, "template": args.get("template").cloned()});+    let mut call_args = json!({"type": kind, "template": args.get("template").cloned(), "visible": visible});+    // A HIDDEN document gets its scratch file from the host, through the very+    // document object it created: it is not necessarily the ACTIVE document, so+    // "save the active one" (give_identity) could name somebody else's.+    if !visible && !arg_bool(args, "keepUnsaved", false) {+        let ext = match kind.as_str() {+            "assembly" => "SLDASM",+            "drawing" => "SLDDRW",+            _ => "SLDPRT",+        };+        let n = crate::scratch::next_scratch_seq();+        let path = crate::scratch::scratch_docs_dir().join(format!("hidden-{}-{n}.{ext}", sanitise_stem(&kind)));+        call_args["scratchPath"] = json!(path.to_string_lossy());+    }     let started = std::time::Instant::now();     match ctx.call("new_document", call_args, 110) {         // A FAILED new_document IS THE EXHAUSTION SIGNAL. On 2026-09-03 both the@@ -3791,12 +3845,22 @@ pub fn new_document(ctx: &Ctx, args: &Value) -> Value {         // an ordinary template problem — which sent the caller looking for a         // fault in a template that was fine. Look at what the machine is         // actually holding before blaming the template.-        Err(e) => match exhausted_error(ctx, &e) {-            Some(v) => v,-            None => sidecar_error(e),-        },+        Err(e) => {+            // A HIDDEN CREATION THAT FAILED still flipped a session setting: it+            // must be back on before anything else happens.+            let settled = e.data.as_ref().and_then(|d| d.get("visibility")).map(|v| settle_document_visible(ctx, &kind, v));+            let mut out = match exhausted_error(ctx, &e) {+                Some(v) => v,+                None => sidecar_error(e),+            };+            if let (Some(v), Value::Object(m)) = (settled, &mut out) {+                m.insert("visibility".into(), v);+            }+            out+        }         Ok(r) => {             crate::ledger::record_time("swx_new_document", started.elapsed().as_secs_f64());+            let settled = r.get("visibility").map(|v| settle_document_visible(ctx, &kind, v));             let mut identity = json!({"identity": "unknown"});             if let Some(t) = r.get("created").and_then(|c| c.get("title")).and_then(|t| t.as_str()) {                 let t = t.to_string();@@ -3835,9 +3899,13 @@ pub fn new_document(ctx: &Ctx, args: &Value) -> Value {                     "title": r.get("created").and_then(|c| c.get("title")).cloned(),                     "type": kind,                     "template": r.get("template").cloned(),+                    "visible": visible,                 }),             );             let mut r = r;+            if let (Some(v), Some(o)) = (settled, r.as_object_mut()) {+                o.insert("visibility".into(), v);+            }             if let Some(o) = r.as_object_mut() {                 if let Some(c) = o.get_mut("created").and_then(|v| v.as_object_mut()) {                     if let Some(t) = identity.get("title").and_then(|v| v.as_str()) {@@ -3855,7 +3923,17 @@ pub fn new_document(ctx: &Ctx, args: &Value) -> Value {                 "documentIdentity": identity,                 "windowEtiquette": "background",                 "progress": job.finished(),-                "_hint": if titled_only {+                "_hint": if !visible {+                    "A NEW HIDDEN document: it has no window, so it costs SOLIDWORKS no graphics \+                     context (the leading suspect for the ~83 MB each created-and-closed document \+                     kept, measured 2026-10-14). Every object-model verb works on it — \+                     swx_run_script, swx_inspect_bodies, swx_mass_properties, the exports, \+                     swx_save_as. SOLIDWORKS documents that a document created hidden cannot be \+                     shown later; to see it, save it and reopen it (swx_save_as, then swx_open). \+                     The hidden-creation setting was put back for the next document \+                     (`visibility.documentVisibleRestored`). UNMEASURED live: whether a hidden \+                     document avoids that cost."+                } else if titled_only {                     "A NEW document is open and active, with a TITLE and NO PATH — so it can only \                      be named by that title, and SOLIDWORKS reuses Part1/Part2 every session. If a \                      second document of this title appears, neither can be addressed by any verb, \@@ -4531,12 +4609,35 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value {     };     // Parse + validate HERE, before anything touches the live session: a script     // that cannot work must never half-run on someone's CAD application.-    let ops = match crate::script::parse(&script) {+    let mut ops = match crate::script::parse(&script) {         Ok(o) => o,         Err(e) => return script_help(e),     };+    let mut script = script;+    // REUSE ONE DOCUMENT FOR A BATTERY (0.10.6). Measured 2026-10-14: every+    // created-and-closed document cost SOLIDWORKS ~83 MB and ~125 handles for+    // good. `reuseDocument` runs the script in a part the BRIDGE created, its+    // leading new_part becoming clear_part — N checks, ONE document.+    let reuse = arg_str(args, "reuseDocument").map(String::from);+    if let Some(r) = &reuse {+        if ops.iter().any(|o| matches!(o, crate::script::Op::NewAssembly { .. })) {+            return script_help("reuseDocument reuses a PART: a script that makes an assembly cannot run in it");+        }+        let news = ops.iter().filter(|o| matches!(o, crate::script::Op::NewPart { .. })).count();+        let leads = matches!(ops.first(), Some(crate::script::Op::NewPart { .. }));+        if news > 1 || (news == 1 && !leads) {+            return script_help("with reuseDocument, new_part may only be the FIRST step (it becomes clear_part)");+        }+        if leads {+            ops[0] = crate::script::Op::ClearPart;+            if let Some(first) = script.as_array_mut().and_then(|a| a.first_mut()) {+                *first = json!({"op": "clear_part"});+            }+        }+        let _ = r;+    }     let makes_its_own_doc =-        matches!(ops.first(), Some(crate::script::Op::NewPart | crate::script::Op::NewAssembly));+        matches!(ops.first(), Some(crate::script::Op::NewPart { .. } | crate::script::Op::NewAssembly { .. }));     let has_doc = ctx         .call("docs", json!({}), 30)         .ok()@@ -4562,10 +4663,37 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value {     let mut target_owned: Option<bool> = None;     let mut pinned_path: Option<String> = None;     if !makes_its_own_doc {-        let target = match resolve_document(ctx, args, "swx_run_script") {+        let target_args = match &reuse {+            Some(r) if r.contains('\\') || r.contains('/') => json!({"path": r}),+            Some(r) => json!({"targetDocument": r}),+            None => args.clone(),+        };+        let target = match resolve_document(ctx, &target_args, "swx_run_script") {             Ok(t) => t,             Err(e) => return e,         };+        if reuse.is_some() && !target.owned {+            return err_code(+                "reuse_not_owned",+                format!("reuseDocument names '{}', which this bridge did not create", target.title),+                "reuseDocument clears and rebuilds the part, so it only ever names a part the bridge made \+                 (swx_new_document {\"type\":\"part\",\"visible\":false} makes one). Nothing was changed.",+            );+        }+        // CLEAR_PART DELETES EVERY FEATURE: only in a part the bridge created,+        // whatever write grant the call carries (0.10.6).+        let clears_before_making = ops+            .iter()+            .position(|o| matches!(o, crate::script::Op::ClearPart))+            .map(|i| !ops[..i].iter().any(|o| matches!(o, crate::script::Op::NewPart { .. })))+            .unwrap_or(false);+        if clears_before_making && !target.owned {+            return err_code(+                "clear_part_not_owned",+                format!("clear_part deletes every feature, and '{}' is not a part this bridge created", target.title),+                "clear_part only ever runs in the bridge's own scratch parts. Nothing was changed.",+            );+        }         target_owned = Some(target.owned);         pinned_path = target.path.clone();         grant = match crate::writes::authorize_in(ctx, args, "swx_run_script", crate::writes::Kind::ModifyDocument, Some(&target), None) {@@ -4591,6 +4719,8 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value {     let call_args = crate::writes::with_grant(json!({"script": script, "targetDocument": pinned, "targetPath": pinned_path}), &grant);     match ctx.call("run_script", call_args, 280) {         Err(e) => {+            // A HIDDEN new_part that failed still flipped a session setting.+            let settled = e.data.as_ref().and_then(|d| d.get("visibility")).map(|v| settle_document_visible(ctx, "part", v));             // A SCRIPT THAT FAILED AFTER ITS new_part STILL MADE THAT PART: the             // bridge owns it, so swx_close_doc / swx_reset_workspace can close it             // (0.10.2 — a failed self-test check left one open, un-closable).@@ -4603,6 +4733,9 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value {             if let (Value::Object(m), Some(c)) = (&mut out, created) {                 m.insert("createdBeforeFailing".into(), c);             }+            if let (Value::Object(m), Some(v)) = (&mut out, settled) {+                m.insert("visibility".into(), v);+            }             out         }         Ok(mut r) => {@@ -4611,6 +4744,16 @@ 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);+            // 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()) {+                for st in steps.iter_mut() {+                    let kind = if st.get("op").and_then(|o| o.as_str()) == Some("new_assembly") { "assembly" } else { "part" };+                    if let Some(v) = st.pointer("/result/visibility").cloned() {+                        st["result"]["visibility"] = settle_document_visible(ctx, kind, &v);+                    }+                }+            }             if let Some(t) = r                 .get("created")                 .and_then(|c| c.get("created").and_then(|x| x.get("title")).or_else(|| c.get("title")))@@ -4691,6 +4834,14 @@ fn script_help(message: impl Into<String>) -> Value {     );     if let Value::Object(map) = &mut v {         map.insert("ops".into(), json!(crate::script::OPS));+        map.insert(+            "verbArgs".into(),+            json!({+                "script": "the steps (required)",+                "targetDocument / path": "the document a script with no new_part runs in",+                "reuseDocument": "the title or path of a part the BRIDGE created: the script runs in it and its leading new_part becomes clear_part — a battery of N checks costs ONE document (each created-and-closed document cost SOLIDWORKS ~83 MB, measured 2026-10-14)",+            }),+        );         map.insert(             "example".into(),             json!({@@ -4708,8 +4859,9 @@ fn script_help(message: impl Into<String>) -> Value {         map.insert(             "opArgs".into(),             json!({-                "new_part": {},-                "new_assembly": {},+                "new_part": {"visible": "true (default) | false — created HIDDEN, with no window (ISldWorks::DocumentVisible); every object-model op works on it, and SOLIDWORKS cannot show it later (save and reopen to see it)"},+                "new_assembly": {"visible": "as new_part"},+                "clear_part": {"(none)": "deletes EVERY feature after the origin of a part the BRIDGE created — never anyone else's; N checks then cost one document"},                 "sketch": {"plane": "Front Plane|Top Plane|Right Plane|<any plane name> | {offsetFrom, offsetMm}", "face": "{atPointMm:[x,y,z]} instead of plane", "entities": "[{rect:{widthMm, heightMm, xMm, yMm, anchor:corner|center, centerMm:[x,y]}}, {circle:{radiusMm|diameterMm, xMm, yMm | centerMm}}] — ALL in one sketch: concentric circles make a ring, a rect with circles inside makes a plate with holes"},                 "sketch_rect": {"plane": "as sketch", "face": "as sketch", "widthMm": "required", "heightMm": "required", "xMm": 0, "yMm": 0, "anchor": "corner (default: xMm/yMm is the CORNER) | center", "centerMm": "[x, y] (implies center)"},                 "sketch_circle": {"plane": "as sketch", "face": "as sketch", "radiusMm": "required (or diameterMm)", "xMm": 0, "yMm": 0, "centerMm": "[x, y]"},
tests/e2e.rs+11
@@ -859,6 +859,17 @@ fn every_verb_over_http_against_the_real_binary() {     );     check!(b, "verb_times gives p50 and p90", r["stages"]["swx_run_script"]["p90"].is_number(), s(&r)); +    // ── the reference diagnostic (0.10.6) ───────────────────────────────────+    let docs_before = b.call("swx_docs", json!({}))["docs"].as_array().map(|a| a.len());+    let r = b.call("swx_com_respawn", json!({"reason": "release every COM reference the bridge holds"}));+    check!(+        b,+        "swx_com_respawn replaces the bridge's own com host and leaves SOLIDWORKS's documents alone",+        r["respawned"] == true && r["solidworksTouched"] == false+            && b.call("swx_docs", json!({}))["docs"].as_array().map(|a| a.len()) == docs_before,+        s(&r)+    );+     // ── OWNERSHIP: the bridge must not change documents it did not create ───     // The suite has a user-owned document open (gearbox.sldasm, opened by this     // test, never created by the bridge). Every mutating verb must refuse it.
tests/readiness.rs+1−1
@@ -375,7 +375,7 @@ const EXEMPT: &[&str] = &[     "swx_action_ledger", "swx_verb_times", "swx_uninstall", "swx_find_files",     "swx_recover_list", "swx_window_info", "swx_dismiss_blocking_dialogs",     "swx_close_window", "swx_screenshot", "swx_set_driving_badge", "swx_kill", "swx_user_presence", "swx_sign_in",-    "swx_selftest",+    "swx_selftest", "swx_com_respawn", ];  #[test]
tests/visibility.rsadded+374
@@ -0,0 +1,374 @@+//! 0.10.6 — FIXING THE MEMORY GROWTH AT ITS ROOT.+//!+//! MEASURED 2026-10-14 (~21:00Z, hosted 0.10.5, SOLIDWORKS 2026 fresh start,+//! 12 x [new_part + a 20x20x10 box] then swx_reset_workspace): SOLIDWORKS's+//! private bytes went 520 MB -> 913 -> 1191 -> then +83 MB per part to 2035 MB;+//! handles 2845 -> 4539, +~125 per part; GetDocumentCount back at 0 every time.+//! So every created-and-closed document cost ~83 MB and ~125 handles for good.+//!+//! This release: documents the bridge makes for itself are created HIDDEN+//! (ISldWorks::DocumentVisible bracketed around NewDocument — a SESSION+//! setting, put back on every path); a battery reuses ONE part (clear_part);+//! and `swx_com_respawn` settles whether the bridge itself pins anything.+//!+//! All in the SIMULATOR: whether hiding or reuse actually stops the growth is+//! UNMEASURED until it runs live.++mod common;++use common::{Bridge, TempDir};+use serde_json::{json, Value};++fn s(v: &Value) -> String {+    serde_json::to_string_pretty(v).unwrap_or_default()+}++fn boot(tag: &str, extra: &[(&str, &str)]) -> (Bridge, TempDir) {+    let machine = TempDir::new(tag);+    std::fs::write(machine.join("windows.json"), "[]").unwrap();+    let mut env: Vec<(&str, &str)> = vec![("SWX_LAUNCH_FAKE_DIR", Box::leak(machine.path().into_boxed_str()))];+    env.extend_from_slice(extra);+    let b = Bridge::start_with(&env);+    (b, machine)+}++/// Every ISldWorks::DocumentVisible call the simulator saw, in order.+fn visible_calls(b: &Bridge) -> Vec<Value> {+    std::fs::read_to_string(b.scratch.join("document-visible.jsonl"))+        .unwrap_or_default()+        .lines()+        .filter_map(|l| serde_json::from_str(l).ok())+        .collect()+}++fn ledger(b: &Bridge) -> Vec<Value> {+    b.call("swx_action_ledger", json!({"limit": 500}))["actions"].as_array().cloned().unwrap_or_default()+}++fn doc_count(b: &Bridge) -> usize {+    b.call("swx_docs", json!({}))["docs"].as_array().map(|a| a.len()).unwrap_or(usize::MAX)+}++fn box_script(first: Value) -> Value {+    json!([first,+           {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},+           {"op": "extrude", "depthMm": 10}])+}++#[test]+fn a_hidden_document_is_created_with_the_session_setting_bracketed_and_put_back() {+    let (b, _m) = boot("vis-hidden", &[]);+    b.call("swx_start", json!({}));+    let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));+    check!(+        b,+        "swx_new_document visible:false: created hidden, DocumentVisible put back, a scratch file of its own",+        r["success"] == true && r["visibility"]["requested"] == false && r["visibility"]["documentVisibleRestored"] == true+            && r["visibility"]["visibleReadBack"] == false && r["visibility"]["scratchSaved"] == true+            && r["documentIdentity"]["path"].as_str().unwrap_or("").ends_with(".SLDPRT"),+        s(&r)+    );+    let calls = visible_calls(&b);+    check!(+        b,+        "DocumentVisible(false), then DocumentVisible(true) — in that order, for parts",+        calls.len() == 2 && calls[0]["visible"] == false && calls[1]["visible"] == true && calls[0]["type"] == "part",+        s(&json!(calls))+    );+    check!(+        b,+        "the toggle and the hidden creation are ledgered",+        ledger(&b).iter().any(|e| e["action"] == "document_visible_toggled")+            && ledger(&b).iter().any(|e| e["action"] == "document_created" && e["detail"]["visible"] == false),+        s(&json!(ledger(&b)))+    );+    // EVERY OBJECT-MODEL VERB WORKS ON IT.+    let path = r["documentIdentity"]["path"].as_str().unwrap_or("").to_string();+    let built = b.call("swx_run_script", json!({"path": path, "script": [+        {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},+        {"op": "extrude", "depthMm": 10}]}));+    let ib = b.call("swx_inspect_bodies", json!({"path": path}));+    let mp = b.call("swx_mass_properties", json!({"path": path}));+    let out = b.scratch.join("hidden.step");+    let ex = b.call("swx_export_step", json!({"path": path, "outputPath": out.to_string_lossy()}));+    check!(+        b,+        "a script, swx_inspect_bodies, swx_mass_properties and swx_export_step all work on the hidden part",+        built["success"] == true && ib["bodyCount"] == 1 && ib["bodies"][0]["volumeMm3"].as_f64().map(|v| (v - 4000.0).abs() < 1e-6) == Some(true)+            && mp["success"] == true && ex["success"] == true,+        format!("{}\n{}\n{}\n{}", s(&built), s(&ib), s(&mp), s(&ex))+    );+    let shown = b.call("swx_show_part", json!({"path": path}));+    check!(+        b,+        "swx_show_part on it: attempted, read back, and SAID to stay hidden (as SOLIDWORKS documents it)",+        shown["visibility"]["wasVisible"] == false && shown["visibility"]["madeVisible"] == false+            && shown["visibility"]["note"].as_str().unwrap_or("").contains("cannot be made visible"),+        s(&shown)+    );+    // THE DEFAULT IS UNCHANGED: a user's document is visible, and nothing toggles.+    let v = b.call("swx_new_document", json!({"type": "part"}));+    check!(+        b,+        "swx_new_document with no visible: a visible document, and no DocumentVisible call at all",+        v["success"] == true && v.get("visibility").is_none() && visible_calls(&b).len() == 2,+        s(&v)+    );+}++#[test]+fn a_hidden_creation_that_fails_still_puts_the_setting_back() {+    let (b, _m) = boot("vis-fail", &[]);+    b.call("swx_start", json!({}));+    std::fs::write(b.scratch.join("mock-newdoc-fails"), "").unwrap();+    let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));+    let calls = visible_calls(&b);+    check!(+        b,+        "NewDocument failed inside the bracket: an error — and DocumentVisible was put back anyway",+        r["success"] == false && r["visibility"]["documentVisibleRestored"] == true+            && calls.len() == 2 && calls[0]["visible"] == false && calls[1]["visible"] == true,+        format!("{}\n{}", s(&r), s(&json!(calls)))+    );+    std::fs::remove_file(b.scratch.join("mock-newdoc-fails")).unwrap();+    let v = b.call("swx_new_document", json!({"type": "part"}));+    let shown = b.call("swx_show_part", json!({"path": v["documentIdentity"]["path"]}));+    check!(+        b,+        "...so the next ordinary document is VISIBLE",+        v["success"] == true && shown["visibility"]["wasVisible"] == true,+        format!("{}\n{}", s(&v), s(&shown))+    );+}++#[test]+fn a_setting_the_host_could_not_put_back_is_retried_and_otherwise_shouted() {+    let (b, _m) = boot("vis-retry", &[]);+    b.call("swx_start", json!({}));+    // The host's three restores fail; the server's second chance works.+    std::fs::write(b.scratch.join("mock-docvisible-restore-fails"), "").unwrap();+    let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));+    check!(+        b,+        "the server retried the restore, it worked, and the retry is ledgered",+        r["success"] == true && r["visibility"]["serverRetry"]["restored"] == true+            && r["visibility"]["documentVisibleRestored"] == true+            && ledger(&b).iter().any(|e| e["action"] == "document_visible_restored_on_retry"),+        s(&r)+    );+    std::fs::write(b.scratch.join("mock-docvisible-restore-fails-always"), "").unwrap();+    let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));+    check!(+        b,+        "a restore that cannot be done is a WARNING to tell the user, and ledgered",+        r["visibility"]["documentVisibleRestored"] == false+            && r["visibility"]["warning"].as_str().unwrap_or("").contains("INVISIBLE")+            && ledger(&b).iter().any(|e| e["action"] == "document_visible_restore_failed"),+        s(&r)+    );+}++#[test]+fn a_users_script_stays_visible_by_default_and_new_part_can_hide() {+    let (b, _m) = boot("vis-script", &[]);+    b.call("swx_start", json!({}));+    let r = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part"}))}));+    check!(+        b,+        "new_part with no visible: visible, and no DocumentVisible call (Michael wants to see his part)",+        r["success"] == true && r["steps"][0]["result"].get("visibility").is_none() && visible_calls(&b).is_empty(),+        s(&r)+    );+    let r = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part", "visible": false}))}));+    check!(+        b,+        "new_part visible:false: hidden, the setting put back, and its variant is UNMEASURED live",+        r["success"] == true && r["steps"][0]["result"]["visibility"]["documentVisibleRestored"] == true+            && r["steps"][0]["result"]["proven"]["variants"] == json!(["new_part.invisible"])+            && r["steps"][0]["result"]["proven"]["live"] == false,+        s(&r)+    );+    let bad = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part", "visible": "no"}))}));+    check!(b, "visible must be a boolean", bad["errorCode"] == "bad_script", s(&bad));+}++#[test]+fn a_battery_reuses_one_part_and_clear_part_never_touches_anyone_elses() {+    let (b, _m) = boot("vis-reuse", &[("SWX_ALLOW_USER_WRITES", "1")]);+    b.call("swx_start", json!({}));+    let made = b.call("swx_new_document", json!({"type": "part", "visible": false}));+    let p = made["documentIdentity"]["path"].as_str().unwrap_or("").to_string();+    let first = b.call("swx_run_script", json!({"reuseDocument": p, "script": box_script(json!({"op": "new_part"}))}));+    let docs = doc_count(&b);+    let second = b.call("swx_run_script", json!({"reuseDocument": p, "script": [+        {"op": "new_part"},+        {"op": "sketch_circle", "plane": "Top Plane", "radiusMm": 5},+        {"op": "extrude", "depthMm": 10}]}));+    let ib = b.call("swx_inspect_bodies", json!({"path": p}));+    check!(+        b,+        "reuseDocument: the leading new_part became clear_part; the box is gone, only the cylinder is left",+        first["success"] == true && second["success"] == true && second["steps"][0]["op"] == "clear_part"+            && second["steps"][0]["result"]["featuresDeleted"] == 1+            && ib["bodyCount"] == 1+            && ib["bodies"][0]["volumeMm3"].as_f64().map(|v| (v - std::f64::consts::PI * 250.0).abs() < 1e-3) == Some(true),+        format!("{}\n{}", s(&second), s(&ib))+    );+    check!(b, "...and no document was created by the reused runs", doc_count(&b) == docs, format!("{docs} -> {}", doc_count(&b)));+    // A USER'S part.+    let user = "C:\\mock\\plate.sldprt";+    b.call("swx_open", json!({"path": user}));+    let r = b.call("swx_run_script", json!({"path": user, "script": [{"op": "clear_part"}],+        "allowUserWrites": true, "confirm": true, "reason": "the user asked to clear it"}));+    check!(+        b,+        "clear_part on a part the bridge did not create: refused by the server, write grant or not",+        r["errorCode"] == "clear_part_not_owned" && r["error"].as_str().unwrap_or("").contains("is not a part this bridge created"),+        s(&r)+    );+    let r = b.call("swx_run_script", json!({"reuseDocument": user, "script": box_script(json!({"op": "new_part"}))}));+    check!(b, "reuseDocument naming the user's part: refused", r["errorCode"] == "reuse_not_owned", s(&r));+    let r = b.call("swx_run_script", json!({"reuseDocument": p, "script": [{"op": "new_part"}, {"op": "new_part"}]}));+    check!(b, "with reuseDocument, new_part only as the first step", r["errorCode"] == "bad_script", s(&r));+}++#[test]+fn com_respawn_replaces_only_the_bridges_host_and_never_calls_solidworks() {+    let (b, _m) = boot("vis-respawn", &[("SWX_MOCK_SLOW_MS", "2500")]);+    b.call("swx_start", json!({}));+    b.call("swx_new_document", json!({"type": "part"}));+    let before = b.call("swx_status", json!({}));+    let docs = doc_count(&b);+    let r = b.call("swx_com_respawn", json!({}));+    check!(b, "swx_com_respawn needs a reason", r["errorCode"] == "reason_required", s(&r));+    std::fs::write(b.scratch.join("mock-log-commands"), "").unwrap();+    let r = b.call("swx_com_respawn", json!({"reason": "measure handles before and after"}));+    let reached = std::fs::read_to_string(b.scratch.join("mock-commands.log")).unwrap_or_default();+    check!(+        b,+        "swx_com_respawn: a new host, the old one left gracefully, and NOTHING reached SOLIDWORKS",+        r["success"] == true && r["respawned"] == true && r["solidworksTouched"] == false && r["gracefulExit"] == true+            && r["oldHostPid"] == before["com"]["hostPid"] && r["newHostPid"] != r["oldHostPid"]+            && reached.trim().is_empty(),+        format!("{}\nreached SOLIDWORKS: {reached:?}", s(&r))+    );+    let after = b.call("swx_status", json!({}));+    check!(+        b,+        "...the same SOLIDWORKS session with the same documents, now served by the new host; ledgered",+        after["com"]["hostPid"] == r["newHostPid"] && after["com"]["processId"] == before["com"]["processId"]+            && doc_count(&b) == docs+            && ledger(&b).iter().any(|e| e["action"] == "com_host_respawn" && e["detail"]["reason"] == "measure handles before and after"),+        format!("{}\n{}", s(&before["com"]), s(&after["com"]))+    );+    // MID-VERB: a slow verb holds the host; the respawn refuses.+    let refused = std::thread::scope(|sc| {+        let slow = sc.spawn(|| b.call("swx_start", json!({})));+        std::thread::sleep(std::time::Duration::from_millis(700));+        let r = b.call("swx_com_respawn", json!({"reason": "while a verb is running"}));+        let _ = slow.join();+        r+    });+    check!(+        b,+        "swx_com_respawn while a verb holds the host: refused com_host_busy, naming the verb, nothing changed",+        refused["errorCode"] == "com_host_busy" && refused["error"].as_str().unwrap_or("").contains("(start)"),+        s(&refused)+    );+}++#[test]+fn the_selftest_builds_hidden_and_reuses_one_document() {+    let (b, _m) = boot("vis-selftest", &[]);+    b.call("swx_start", json!({}));+    let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03", "T15", "T17"]}));+    let rows = r["results"].as_array().cloned().unwrap_or_default();+    let row = |id: &str| rows.iter().find(|x| x["name"].as_str().unwrap_or("").starts_with(id)).cloned().unwrap_or(Value::Null);+    check!(+        b,+        "every check passes; T01 and T03 ran in the shared part, T15 (a colour) and T17 (a feature by name) in their own",+        r["summary"]["failed"] == 0 && row("T01")["pass"] == true && row("T03")["pass"] == true+            && row("T01")["reusedDocument"] == true && row("T03")["reusedDocument"] == true+            && row("T15")["reusedDocument"] == false && row("T17")["reusedDocument"] == false,+        s(&r)+    );+    check!(+        b,+        "documentsCreated: one shared part, two of their own, one scratch part for the verbs — 4, not 5",+        r["documentsCreated"] == 4,+        s(&r["documentsCreated"])+    );+    let calls = visible_calls(&b);+    let hidden = calls.iter().filter(|c| c["visible"] == false).count();+    check!(+        b,+        "every one of them was created HIDDEN, and the setting was put back after each",+        hidden == 4 && calls.len() == 8 && calls.chunks(2).all(|p| p[0]["visible"] == false && p[1]["visible"] == true),+        s(&json!(calls))+    );+}++/// THE MEASURED GROWTH, SIMULATED: every created document leaves SOLIDWORKS+/// 83 MB heavier (`growthPerDocumentMB`). A reused check creates nothing, so+/// it costs nothing; and when the budget is reached mid-battery, the run+/// closes its own shared part FIRST, so its bridge-started session can be+/// recycled (an open document would stop the run instead).+#[test]+fn reuse_keeps_solidworks_flat_and_the_shared_part_is_closed_before_a_recycle() {+    let machine = TempDir::new("vis-growth");+    std::fs::write(machine.join("windows.json"), "[]").unwrap();+    std::fs::write(machine.join("quit-script.json"), json!({"a": "exited"}).to_string()).unwrap();+    std::fs::write(machine.join("sldworks-created"), "1700000000").unwrap();+    std::fs::write(+        machine.join("sldworks.json"),+        json!({"responding": true, "hwnd": 6622700, "privateMB": 520, "growthPerDocumentMB": 83, "handles": 2845}).to_string(),+    )+    .unwrap();+    std::fs::write(+        machine.join("memory.json"),+        json!({"commitLimitMB": 78438, "commitAvailMB": 40000, "physTotalMB": 32153, "physAvailMB": 9000, "processes": []}).to_string(),+    )+    .unwrap();+    let scratch = TempDir::new("vis-growth-scratch");+    std::fs::write(scratch.join("mock-solidworks-session"), "700900").unwrap();+    std::fs::write(+        scratch.join("launch-state.json"),+        json!({"attempt": {"id": 1, "startedAt": 1700000000u64, "shortcut": "x.lnk", "route": "shellExecute",+                           "invokedAt": 1700000000u64, "stage": "attached", "finished": true, "outcome": "attached",+                           "runBeginSeen": true, "attributedSolidworks": {"pid": 700900, "createdAt": 1700000000u64}},+               "preferredRoute": "shellExecute", "consecutiveFailures": 0})+        .to_string(),+    )+    .unwrap();+    let b = Bridge::start_in(+        scratch,+        &[+            ("SWX_LAUNCH_FAKE_DIR", Box::leak(machine.path().into_boxed_str())),+            ("SWX_QUIT_SETTLE_MS", "80"),+            ("SWX_QUIT_EXIT_WAIT_SECS", "3"),+            ("SWX_RECYCLE_POLL_MS", "100"),+            ("SWX_RECYCLE_READY_SECS", "30"),+            // 600 MB: the shared part (one document, 603 MB) already passes it.+            ("SWX_SW_MEMORY_BUDGET_MB", "600"),+        ],+    );+    b.call("swx_start", json!({}));+    let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03", "T05"]}));+    let rows = r["results"].as_array().cloned().unwrap_or_default();+    let row = |id: &str| rows.iter().find(|x| x["name"].as_str().unwrap_or("").starts_with(id)).cloned().unwrap_or(Value::Null);+    let rec = r["recycled"].as_array().cloned().unwrap_or_default();+    check!(+        b,+        "after T01 built the shared part (603 MB > 600), the run CLOSED its own part and recycled before T03 — it did not stop",+        r["complete"] == true && rec.len() == 1 && rec[0]["beforeCheck"] == "T03" && rec[0]["solidworksPrivateMbBefore"] == 603,+        s(&r)+    );+    check!(+        b,+        "every check passed, T01 in the first shared part and T03/T05 in a new one",+        r["summary"]["failed"] == 0 && row("T01")["swPrivateMbAfter"] == 603+            && row("T03")["reusedDocument"] == true && row("T05")["reusedDocument"] == true,+        s(&json!(rows))+    );+    check!(b, "nothing was terminated", !machine.join("terminated.jsonl").exists(), "terminated.jsonl");+}
tests/wiring.rs+1−1
@@ -1069,7 +1069,7 @@ fn the_startup_exemption_list_is_pinned_name_by_name() {         "swx_action_ledger", "swx_verb_times", "swx_uninstall", "swx_find_files",         "swx_recover_list", "swx_window_info", "swx_dismiss_blocking_dialogs",         "swx_close_window", "swx_screenshot", "swx_set_driving_badge", "swx_kill", "swx_user_presence", "swx_sign_in",-        "swx_selftest",+        "swx_selftest", "swx_com_respawn",     ];     pinned.sort();     assert_eq!(
uninstall.sh