subagent transcript
session 17f09f89 · workflow wf_1ced79bb
delegated task
FreeCAD is ported to WebAssembly (JSPI + native-EH, static CPython 3.14, no dlopen, no subprocess, single-thread, Qt 6.11). 17 workbenches already wor
▸ Magik
FreeCAD is ported to WebAssembly (JSPI + native-EH, static CPython 3.14, no dlopen, no subprocess, single-thread, Qt 6.11). 17 workbenches already work incl. Part/PartDesign/Sketcher/Measure/Draft/Assembly/TechDraw/Mesh + numpy + PySide6 + pivy._coin. Source tree: /home/magik6k/lcad-wasm/freecad-port/FreeCAD (branch wasm-parity-push). Build tree (already configured, DO NOT build — read only): /opt/toolchains/src/freecad-gui-build. The module-enablement pipeline is documented in the memory files at /home/magik6k/.claude/projects/-home-magik6k-lcad-wasm/memory/ (read freecad-wasm-parity-roadmap.md and freecad-wasm-viewport.md FIRST — they describe the src/Main/CMakeLists.txt inittab generator, FREECAD_WASM_EXTRA_PY_MODULES / FREECAD_WASM_EXTRA_LINK_LIBS, SHARED->STATIC conversion, data-packaging with file_packager, and known gotchas).
Constraints that shape feasibility: NO subprocess/fork/exec, NO dlopen (all C-ext modules static via inittab), NO threads (QThreadPool/QtConcurrent absent), NO listen sockets, wasm32 4GB ceiling. C++ deps must be new-EH (-fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0). Pure-Python addons are fine; native ones need static linking.
Your job: READ ONLY. Do not edit source, do not run cmake/ninja/emcc. Produce an actionable plan a build engineer can execute.
Solve the DEFERRED persistence RELOAD-LOAD bug. SAVE works (user.cfg lands at /tmp/.config/FreeCAD/v26-3/user.cfg + IDBFS persists across reload), but on reload the in-memory ParameterManager reads prev_marker=<none> — the hydrated user.cfg is not loaded into the live manager. Root-cause it precisely: is it (a) IDBFS syncfs(true) hydrate not completing before initConfig/LoadParameters (the index.html addRunDependency gate not actually holding main), or (b) LoadDocument not repopulating GetGroup? Read src/App/Application.cpp (LoadParameters, the __EMSCRIPTEN__ re-point + create_dirs + LoadDocument), src/Base/Parameter*.cpp (ParameterManager LoadDocument/serializer), deploy/index.html (the fc-idbfs-hydrate addRunDependency + FS.syncfs(true) gate + preRun ordering). Give the exact fix (file + edit) and how to verify. This is high-value (user prefs/recent-files persistence).
Fable
thinking
I'm diving into a persistence bug in the FreeCAD WebAssembly port and need to examine the relevant code to understand what's going wrong.
Fable
⚙ Bash {"command": "cat /home/magik6k/.claude/projects/-home-magik6k-lcad-wasm/memory/freecad-wasm-viewport.md", "description": "Read viewport memory file"}
Fable
⚙ Bash {"command": "ls -la /home/magik6k/lcad-wasm/freecad-port/FreeCAD/deploy/ 2>/dev/null; echo \"---\"; find /home/magik6k/lcad-wasm/freecad-port/FreeCAD/deploy -name \"index.html\" 2>/dev/null", "description": "List deploy dir"}
Fable
thinking
The deploy directory appears to be empty, so I need to locate where the actual files are stored.
Fable
⚙ Bash {"command": "ls deploy 2>/dev/null; echo \"=== find index.html ===\"; find /home/magik6k/lcad-wasm -maxdepth 4 -name \"index.html\" 2>/dev/null; echo \"=== find deploy dirs ===\"; find /home/magik6k/lcad-wasm -maxdepth 3 -type d -name deplo…
Fable
⚙ Bash {"command": "grep -n \"EMSCRIPTEN\\|LoadParameters\\|syncfs\\|IDBFS\" src/App/Application.cpp | head -80", "description": "Grep Application.cpp for relevant symbols"}
Fable
thinking
I need to look at the Application.cpp file to understand how parameters are loaded, particularly focusing on the EMSCRIPTEN-specific sections and the main port directory at /home/magik6k/lcad-wasm/freecad-port/FreeCAD.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp", "offset": 2820, "limit": 180}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp", "offset": 3345, "limit": 140}
Fable
thinking
I'm tracing through the reload sequence and noticing the prev_marker shows as none even though the file should be hydrated. The issue seems to be a timing problem where the IDBFS syncfs call hasn't finished populating the filesystem before the ParameterManager tries to load the configuration.
Looking at the bootstrap order, the managers get created early, then LoadParameters is called which re-points and loads the document again, followed by another re-point and LoadDocument block. The real question is whether the file actually exists on the filesystem at the moment LoadDocument checks it, or if LoadDocument is being called but not properly repopulating the manager's internal state.
I need …
Fable
⚙ Bash {"command": "grep -n \"bootstrapEarly\\|syncfs\\|FS.syncfs\\|IDBFS\\|addRunDependency\\|removeRunDependency\\|fc-idbfs\\|hydrate\\|preRun\\|prev_marker\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp", "description": …
Fable
⚙ Bash {"command": "grep -rn \"bootstrapEarly\\|syncfs\\|prev_marker\\|IDBFS\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/ 2>/dev/null | head -40", "description": "Grep source tree"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp", "offset": 418, "limit": 80}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.h", "offset": 1095, "limit": 30}
Fable
thinking
I'm tracing through the initialization sequence in emscripten—`bootstrapEarly()` gets called during module load from global constructors, which happens when the wasm module instantiates inside `run()` after `preRun` completes and the filesystem is ready, specifically when `__wasm_call_ctors` executes before `main`.
The critical piece is that if IDBFS hydration is set up as a run dependency in `preRun`, then `run()` should wait for that to finish before proceeding to the constructor phase and main. But the bug report suggests this gate isn't actually holding—the global constructors and main are running before the hydration completes. I need to check the actual index.html to see if the addRun …
Fable
⚙ Bash {"command": "grep -n \"syncfs\\|IDBFS\\|addRunDependency\\|removeRunDependency\\|fc-idbfs\\|hydrate\\|preRun\\|mkdir\\|mount\\|FS\\.\\|Module\\b\\|user.cfg\\|\\.config\" /home/magik6k/lcad-wasm/freecad-port/deploy/index.html | head -80", "d…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/deploy/index.html", "offset": 44, "limit": 135}
Fable
thinking
Looking at how Emscripten's preRun callbacks work—they execute in order during the `run()` function before `initRuntime()` is called. The first callback mounts IDBFS and triggers `addRunDependency` plus `syncfs(true)`, which is correct. The critical piece is understanding how `run()` checks the run dependency counter to determine when initialization can proceed.
The execution flow is: `preRun()` callbacks run first, then if `runDependencies > 0`, the function returns and waits. Once `removeRunDependency()` brings the counter to zero, it calls `doRun()`, which runs the global C++ constructors and main. Since the hydrate operation completes before the dependency counter drops, the constructor …
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter*; echo \"===\"; grep -n \"LoadDocument\\|LoadOrCreateDocument\\|CreateDocument\\|SetSerializer\\|::Load\\|Serializer\\|Clear\\|_paramGrp\\|prev_marker\\|CheckDo…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "offset": 1800, "limit": 130}
Fable
thinking
I'm wondering whether `LoadDocument` actually refreshes the cached parameter tree when it updates `_pDocument` and `_pGroupNode`, or if the existing ParameterGrp objects—which likely inherit from ParameterGrp itself—still hold stale references to the old DOM nodes through their internal group map.
The issue becomes clear on wasm: bootstrapEarly loads an empty user.cfg and creates the managers, then global constructors immediately call GetGroup to cache child ParameterGrp objects pointing into that empty document's DOM. When initConfig later calls LoadDocument with the real user.cfg, it replaces the document and the manager's own group node, but those already-cached child groups still refere …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "offset": 1651, "limit": 140}
Fable
thinking
I'm examining the ParameterManager constructor to understand how it connects with the ParameterGrp _pGroupNode and how the manager's root group node relates to the ParameterGrp base class.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "offset": 1616, "limit": 40}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "offset": 2031, "limit": 70}
Fable
thinking
I'm investigating whether ParameterManager caches child ParameterGrp references that could become stale when the underlying group node or document changes. I need to examine how ParameterGrp stores its group node and manages the child group map, particularly in the GetGroup implementation.
Fable
⚙ Bash {"command": "grep -n \"_GroupMap\\|_pGroupNode\\|_GetGroup\\|GetGroup\\|class ParameterGrp\\|class ParameterManager\\|_Manager\\|Notify\\|RefreshDocument\\|Reset\\|_Reset\\|revalidate\\|Reload\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "offset": 1280, "limit": 120}
Fable
thinking
I need to check whether `_GetGroup` invalidates the cached child groups in `_GroupMap` when `LoadDocument` updates the manager's `_pDocument` and `_pGroupNode` references.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "offset": 1160, "limit": 120}
Fable
⚙ Bash {"command": "grep -n \"ParameterGrp::_GetGroup\\|ParameterGrp::GetGroup\\|Base::Reference<ParameterGrp> ParameterGrp\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "description": "Find GetGroup impl"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "offset": 380, "limit": 120}
Fable
thinking
So the `_GetGroup` method caches child parameter groups in `_GroupMap`, where each cached group maintains a pointer to a DOM element within the manager's document. The issue is at line 431—when a group already exists in the cache, it returns that cached instance without re-resolving its DOM pointer against the new document, which could be problematic.
The sequence on reload is: bootstrapEarly creates managers with an empty document, global constructors call `GetGroup` and cache child groups pointing into that empty DOM, then initConfig loads the real user config file and updates the manager's document pointer to the new root—but the cached child groups still reference nodes from the old emp …
Fable
⚙ Bash {"command": "grep -n \"_Reset\\|::Notify\\|NotifyAll\\|_pGroupNode =\\|_pGroupNode=\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "description": "Find _Reset and node assignments"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp", "offset": 1548, "limit": 70}
Fable
thinking
I'm working through how cached groups behave when documents are reloaded. The desktop version doesn't rebind cached groups on load since upstream typically doesn't reload documents with live cached groups, but FreeCAD does support reloading through operations like Revert or parameter changes. I need to find where the tree gets re-resolved after LoadDocument, possibly in the calling code or through a specific method that handles rebinding.
Looking at how GetGroup handles a stale `_pGroupNode` — it uses the cached group without re-checking against the current document, so there's no automatic rebind happening. When LoadDocument updates the manager's `_pGroupNode` to point to the new Root, the …
Fable
⚙ Bash {"command": "grep -rn \"prev_marker\\|marker\\|SaveParameter\\|saveParameter\\|GetParameterGroupByPath\\|reload\" /home/magik6k/lcad-wasm/freecad-port/deploy/boot.py /home/magik6k/lcad-wasm/deploy/boot.py 2>/dev/null | head -40; echo \"=== …
Fable
⚙ Bash {"command": "grep -n \"marker\\|Param\\|param\\|cfg\\|persist\\|Recent\\|prev\" /home/magik6k/lcad-wasm/freecad-port/deploy/boot.py | head -40", "description": "Search boot.py for param markers"}
Fable
thinking
I'm tracing through the parameter retrieval chain to understand where the staleness issue originates. The `prev_marker` appears to come from a test harness, and when `App.ParamGet` is called with a user parameter, it flows through `Application::GetParameterGroupByPath` and eventually reaches `_pcUserParamMngr->GetGroup()`, which is where the stale data problem surfaces.
Fable
⚙ Bash {"command": "grep -n \"GetParameterGroupByPath\\|GetUserParameter\\|_pcUserParamMngr->GetGroup\\|ParameterManager::GetGroup\\|GetParameterSet\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp | head", "description": "Fi…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/App/Application.cpp", "offset": 1582, "limit": 30}
Fable
thinking
So the issue is that when `App.ParamGet` calls down through `GetParameterGroupByPath` to retrieve a parameter group, it's hitting a cached version that was populated during the empty bootstrap phase. That cached group's internal DOM node still points to the old empty structure, so when it tries to read the parameter value, it finds nothing and `prev_marker` ends up empty. The real question is whether that child group was actually cached during bootstrap—it would only be if something triggered `GetGroup` on that exact path before the actual data loaded.
But even if the specific marker path wasn't touched, the intermediate groups definitely were. When bootstrap code calls `GetGroup("BaseApp/P …
Fable
⚙ Bash {"command": "grep -n \"FindElement\\|CreateElement\\|DOMElement \\*FindElement\\|::FindElement\\|::CreateElement\\|static.*FindElement\\|static.*CreateElement\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.cpp | head; ech…
Fable
thinking
I need to examine the signatures of FindElement and CreateElement, then look at the header section around lines 435-495 to figure out how to design and place the rebind method.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.h", "offset": 395, "limit": 100}
Fable
thinking
I need to check the ParameterManager class declaration to find where to add a public method and verify it inherits from ParameterGrp, then look at the LoadDocument declarations.
Fable
⚙ Bash {"command": "grep -n \"class BaseExport ParameterManager\\|class ParameterManager\\|public ParameterGrp\\|int LoadDocument\\|void CreateDocument\\|LoadOrCreateDocument\\|void SaveDocument\\|RefreshGroups\\|NotifyAll\" /home/magik6k/lcad-was…
Fable
thinking
I've confirmed that `ParameterManager` inherits from `ParameterGrp`. The core issue is that after rebinding, observers like `ViewParams` need to be notified of the new values since they cache data in their members, but the immediate symptom is that direct calls to `App.ParamGet(...).GetInt(...)` return stale values until rebind is triggered. Once rebind happens, the direct read works, though cached observer values still need notification to update.
I'm considering whether a simpler fix would be to destroy and recreate the managers fresh during the wasm reload, but that would orphan the existing `ParameterGrp::handle` references that `ViewParams` and other global-constructor code already hol …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Base/Parameter.h", "offset": 537, "limit": 60}
Fable
thinking
I'm finalizing the code changes with precise diffs, starting with adding documentation to Parameter.h that explains how cached sub-group handles get re-resolved when the underlying XML document reloads.
Writing the implementation...
The fix is straightforward—roughly 1-2 hours of work including rebuild and verification, with high impact since it eliminates the root cause entirely. No blocking issues, just a tractable solution.
Fable
I have root-caused the bug definitively. Let me compile the findings.
Fable
⚙ StructuredOutput {"area": "FreeCAD-wasm DEFERRED persistence: reload does not repopulate the live ParameterManager (user.cfg/prefs/recent-files)", "verdict": "tractable-now", "effortHours": "1-2", "valueToUsers": "high", "filesInspected": ["/home/magik6k/lc…