main
Drew Owens 0.9.12 source 97dcc9e 19d ago

Closing, quitting and recovery — swx

Read this before closing any document, quitting SOLIDWORKS, draining leaked imports, or telling the user where their closed work is. Part of the solidworks-bridge skill — SKILL.md beside this folder has the rules that never bend. Trust each verb's own reply (_hint, errorCode) over anything written here.

Closing and quitting: never ask the user to do it

  • The bridge never asks you to close. swx_shutdown {reason} saves every open document into a recovery folder, closes them all, and quits SOLIDWORKS. No write opt-in, no per-document confirmation, no environment flag — the user has already authorized this standingly. It needs a reason, because the reason is written for them.
  • The safety property is a SEQUENCE, not a question: nothing closes before it is on disk. Every document — theirs or the bridge's, saved or not — is written to %LOCALAPPDATA%\adom-solidworks-bridge\recovery\<timestamp>\ and verified there before anything is closed. A document that could NOT be written is not closed, and the response names it in remaining.
  • The user's own files are never written over. A document they had saved and then edited is copied into recovery under a new name; their file on disk is untouched and their unsaved changes still survive.
  • It refuses to start a sweep that will not fit. Before writing anything it estimates the session — by document kind, since an assembly is about thirty times the median part — and compares it with free space on the recovery volume; if there is less than twice the estimate it answers insufficient_headroom with the numbers, having written and closed NOTHING. Free some space, or point recoveryDir at a volume with room. It re-checks before every save, so a disk that fills mid-sweep stops it too — what was already written stays written and nothing is closed. headroomBytesBefore and estimatedWriteBytes are in every reply. The reason is a truncation nobody can detect afterwards: a recovery file can be the RIGHT document CUT SHORT and nothing in the file would show it.
  • "On disk" is checked structurally. Each recovery file must LOOK like a SOLIDWORKS document (the container marker at its head) and be at least as big as the smallest real document of its kind (part 24 KB, assembly 80 KB, drawing 256 KB — and if the bridge could not establish the kind, the strictest of the three plus at least the size of the document's own previous file); both are reported per document as recoverySignatureOk and recoveryBytes. A full disk leaves zero-byte and truncated files that exists() and any byte floor accept; those are not saves, and the document stays open. The check fails CLOSED — anything unrecognised leaves the document open rather than closing it against a file nobody can vouch for.
  • A save prompt cannot appear for the reason a close would raise one, because nothing is unsaved by the time a close happens. But a SAVE can raise one of its own (a missing external reference, a rebuild question), so the window census runs before EVERY save and before EVERY close. If ANY dialog appears the sweep stops where it is, reports it in stoppedByDialog, and does nothing further. It is never answered. Everything already written stays written.
  • If the window census cannot be READ, nothing closes. Unknown is not "no dialog". The saves still run — saving loses nothing — but no document is closed and the reply carries stoppedBy:"dialogCensusUnknown", dialogCensusReadable:false, done:false. The work is already safe in the recovery folder; tell the user that, and run it again when swx_status shows the session is readable.
  • swx_close_all_safely {reason} is the same sweep without the quit, for when the user wants an empty session rather than no SOLIDWORKS.
  • swx_recover_list shows what every sweep preserved and where, newest first. Tell the user this path when you close their work. CHECK verified ON EVERY FILE — and note source: "manifest" is the sweep's own verdict, "reverified" is this bridge re-checking a folder that left no record, where verified:null means UNKNOWN rather than good. verified:false means the sweep refused to close against those bytes and the document was left OPEN — that work is still in SOLIDWORKS, and telling the user it was rescued would be false. Then FOLLOW EACH FILE'S OWN howToOpen: most say swx_open, but a file named .sldrecover is the rescue of a document whose kind could not be established, and SOLIDWORKS will not open it until it is renamed to .SLDPRT, .SLDASM or .SLDDRW. Never tell the user to swx_open one of those.
  • A clean, saved document gets no copy — and is still listed. Its own file is its preservation, so the sweep closes it at its own path and never writes to that file. Each sweep's closedInPlace lists those documents with originalPath, closed, present and untouched (the file measured against what the sweep recorded). When the user asks where such a file went, answer from that entry: "closed at its own path, untouched" is only true when untouched is true; false means something saved it afterwards, and present:false means it is no longer at that path — say which.
  • Older locations are listed too, and say what can take them. Before 0.9.1 the data lived inside ab's directories: 0.8.x in the bridge cache, which every bridge update re-extracts (location:"legacyInstallCache", atRiskFromUpdate:true), and 0.9.0 in Adom Bridge\solidworks-data, which ab's installer and uninstaller own (location:"legacyDataRoot"). Both carry atRisk:true. If a sweep's copiedTo is set, use that copy; if it is null, that folder is the only copy, and the user should hear that before anything updates or reinstalls. In the data root, a file marked notCopiedYet exists only at its legacyPath (same warning); one marked partialCopy is an interrupted copy — incomplete, never a rescue — and the file it was copying says partialCopyOnly. A file copiedBeside (<name>.legacy-<stamp>) is a DIFFERENT version of besideOf that an older location held after the first copy; migration.changedSinceCopy lists every such change. Check both before relying on either.
  • The ledger and the ownership registry are MERGED, not picked. When two versions exist — one in each older location, or one that changed after it was copied — the live ledger.jsonl holds every line of both (once each, in at order) and the live owned-docs.json every claim of both; both versions are kept beside it and migration.merged says where. So swx_action_ledger shows every era of history, including a rollback's. A version that could not be merged is under migration.mergeFailed, kept beside, never dropped.
  • Three refusals that wrote and closed NOTHING, and are not save problems: recovery_inside_install_dir (the data root was pointed inside the install directory, ab's own directory, or an older bridge's location), data_root_is_temp (neither LOCALAPPDATA nor USERPROFILE was set, so the data root fell back to the OS temp directory, which Windows empties on a schedule), and recovery_dir_unwritable (the recovery folder could not be created — permissions, a quota, a file where the folder must go; the reply names the cause once, before any document was saved). Say what the reply says; do not retry until the cause is fixed.
  • host_app_gone means SOLIDWORKS died during the sweep — not a dialog, not a disk problem. The reply says how many documents were preserved (each verified) and closed before it went, and quotes the crash markers SOLIDWORKS wrote in its performance log (crashEvidence). Tell the user what was preserved (swx_recover_list), and wait for solidworks.ready after they restart it. lockFiles in a listed sweep are SOLIDWORKS' ~$ lock files: not rescues.
  • A quit declined on an empty session may be PRIMED. MEASURED 2026-10-01: a session that has never held a document can decline ExitApp until one has been opened and closed. Under userAuthorized, on an empty session with no dialog, swx_stop creates and closes one bridge-owned scratch part and asks again; the second attempt in rungsTried says primed:true. Nothing is ever primed while a document is open. swx_status.solidworks.sessionHistory is ADVISORY — it predicts, it never decides; the prime fires on the declined empty quit itself.
  • A NOTIFICATION does not stop anything. A window is judged by measured modality and by its words: it blocks when its title or text names a save, recovery or import prompt, its owner frame is disabled, or it offers a choice (a Button control, or two or more captions like Save / Don't Save / Cancel in any control); a tool window over an enabled frame with nothing to click is a notification (an owner frame that reads disabled only for an instant is re-read first — ownerDisabledTransiently:true) (kind:"notification", blocking:false) — reported under notifications with its evidence, never answered, and the quit, sweep or import goes ahead. The 3DEXPERIENCE "Connected to …" notice is one (measured 2026-10-01). textIsTemplatePlaceholder:true means the captions read were a template's ("Title", "Description", …), not what the window shows — do not quote them to the user as its message.
  • user_is_active — the session is the user's. Every quit and sweep measures presence first (SOLIDWORKS in front with recent input, documents the bridge did not open, or an earlier take-over) and refuses with the evidence under userPresence. A document is the bridge's only if a bridge verb opened or created it — in a session the user launched, every document is theirs, so a quit or sweep there always needs their explicit say-so. Leave the session alone. swx_user_presence reads the same measurement; userPresentOverride:true (with userAuthorized:true and a reason) is only for the user's own explicit request.
  • Assemblies close first; components go with them. The sweep preserves every document, then closes assemblies (the active one first) before parts; a component still held by an open assembly is deferred, and one that went out with its parent is listed in closedByParent. A long sweep answers inside ab's budget with done:false — poll swx_progress (its detail has the stage and counts) and call the sweep again.
  • swx_shutdown is idempotent. SOLIDWORKS not running answers success:true, alreadyStopped:true. It is the last thing you do, and the last thing should be safe to repeat.
  • What did NOT change. swx_stop still quits only a session with zero documents open; the ladder still refuses on a dialog, still measures the exit from the process census, and still never kills anything. The authorization moved the consent question, not the data-safety guarantees.

Quitting, draining and flooded sessions

  • swx_close_all_documents accounts for everything it leaves open. Its skipped array names every document still open when it returns, with why — and a document whose dirtiness SOLIDWORKS will not report is treated as UNSAVED, not clean. Reaching this verb at all needs the write opt-in, since it ends the user's session; a refusal closes nothing.
  • swx_stop quits ONLY an empty session, and proves the exit from the process census. It is a measured ladder, pinned at rung (a) (UserControl=false; ExitApp; release every COM reference); rungs (b)-(f) exist and need an explicit maxRung, and none of them has ever run against a real SOLIDWORKS. Before it tries anything it re-measures seven preconditions, and any one of them coming back UNKNOWN refuses (stop_preconditions_unknown) — an unanswerable question is not permission. The commonest refusal is open_documents_present: documents open means no, and force does not waive it (force waives the UNSAVED question only). Success is this pid with this creation time absent from the census — never what ExitApp returned — and nothing the bridge owns is disowned until that is true, so a declined quit leaves you still able to save and close your own documents. If a dialog appears the ladder stops permanently and reports it (stop_blocked_by_save_prompt / stop_blocked_by_dialog): this bridge never answers a dialog — show the user swx_state's background screenshot and let them answer. A decline is success:false with errorCode:"host_app_stop_declined" and the whole rungsTried list. Do not reach for swx_kill: it discards exactly the work the dialog is asking about, and on a Connected edition relaunching takes 1-3 minutes and can fail. expectedSurvivorsAlive (the 3DEXPERIENCE tray daemons, ENOPLMCSAClient, EdmServerV6) is CONSENT, not a leak — never end one. Every precondition is re-measured before EVERY rung, so a multi-rung ladder can refuse in the middle (refusedMidLadder:true) if a document appears while a rung is settling, and it never continues against a different process (stop_identity_changed).
  • On a flooded session, every list is a SAMPLE and says so. Above the enumeration cap swx_docs returns {count, truncated:true, sample, importTempsInSample, importTempsInSampleOf} and NO docs list — it walks ten documents, it does not enumerate thousands (doing that killed the com host on a live 2144-document session). swx_close_all_documents on such a session still closes, but reports closed/remaining as null with counts, and names the unknown in skipped. Never read a null list as "nothing there".
  • close_outcome_unknown means a HUMAN has to look. It is the drain's third outcome: a close reported success, the document is still open, and the count could not be read — so it cannot be told whether nothing closed or something else did. The drain stops. Do not retry it; ask the user what is on their screen, and read closeFailed for the path and the count immediately before the close.
  • A drain that says done:false is not finished — read walkStop. The drain looks at a bounded WINDOW per call (documentsScanned), and done:true means the walk reached the END of the session with nothing left. walkStop is reachedEnd / hitMax / timedOut / iterationFailed, and titleCollisionsCheckedOver says how many documents the collision check actually covered — a same-titled document beyond the window is not in that list. Call it again until done:true.
  • The drain never closes a document whose title another open one shares — compared the way CloseDoc resolves a name, which on Windows is case-insensitively. If the open set cannot be enumerated it does not attempt the registry sweep at all (ownedUnsavedSweepBlind:true, done:false): it cannot tell whether a title is shared, and a title-only document can only be closed by name. It closes by full path and verifies each close, but CloseDoc resolves by name — so a shared title is refused and reported in titleCollisions (with every path that shares it) rather than letting SOLIDWORKS pick. The user decides: close it themselves, or save the other document under another name. done:false while any collision stands.
  • swx_close_import_temps needs a resolved %TEMP%, and says so. The drain identifies a temp by its LOCATION under this user's resolved temp directory — that path is the whole proof, because the close does not prompt. A path that merely looks like a temp (a restored profile backup, another user's profile) is not enough, and when %TEMP% cannot be resolved the verb refuses with temp_root_unknown rather than reporting that it found nothing to close. Read tempRoot and importTempsElsewhere in the reply: closed:0 with importTempsElsewhere:N means N documents look like leaked temporaries but sit under a directory this bridge could not resolve (ab can run under a different profile or a redirected TEMP), so the drain is NOT done — done:false — and the hint says how to point it at the right root.