Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
e2ac2cc
feat: implement runtime heap for cross-module shared object allocation
Joshpolansky Jun 16, 2026
de1ff5e
feat: add async command handling with CommandChannel and CommandClient
Joshpolansky Jun 16, 2026
c31aa84
feat: enhance CommandClient submit methods for better command handling
Joshpolansky Jun 16, 2026
69c9313
feat: add CommandProbe module and integration tests for async command…
Joshpolansky Jun 16, 2026
76e17b9
Address Copilot review: thread-safe CommandStatus + tests
Joshpolansky Jun 16, 2026
6f9f669
feat: implement CommandFb with PLCopen output semantics and associate…
Joshpolansky Jun 16, 2026
702d246
feat: add Port and PortRef for typed connection points with tests
Joshpolansky Jun 17, 2026
34c9a41
Merge branch 'claude/priceless-moore-2c99b7' into dev
Joshpolansky Jun 19, 2026
67bc115
test: use initGuarded() in module-loader lifecycle tests
Joshpolansky Jun 19, 2026
37b1805
diag: Phase 1 β€” make every crash diagnosable (no new deps)
Joshpolansky Jun 19, 2026
dfa5b58
test: add crasher module for crash-diagnostics verification
Joshpolansky Jun 19, 2026
c8a2273
chore: ignore _crashtest scratch dir
Joshpolansky Jun 19, 2026
2fad66f
feat(diag): in-process stack symbolization via cpptrace
Joshpolansky Jun 19, 2026
e6ad9b4
feat(diag): structured fault reports + /api/faults + loom/faults topic
Joshpolansky Jun 19, 2026
f995360
fix(diag): POSIX build portability + lastFault data race (CI + Copilo…
Joshpolansky Jun 19, 2026
a45748b
fix(diag): address Copilot re-review (UAF, quarantine, signal-safety,…
Joshpolansky Jun 19, 2026
53c6f77
Merge pull request #13 from Joshpolansky/feat/crash-diagnostics
Joshpolansky Jun 19, 2026
4344ce2
feat(diag): symbolized JSON crash reports on POSIX (mac/linux), not j…
Joshpolansky Jun 20, 2026
d438308
build(diag): generate + ship debug symbols for in-process crash symbo…
Joshpolansky Jun 20, 2026
f55744d
fix(build): move macOS .dSYM generation to CI (add_custom_command dir…
Joshpolansky Jun 20, 2026
041e549
fix(diag): unwind from the faulting context on POSIX (keep the leaf f…
Joshpolansky Jun 20, 2026
ad14dee
fix(diag): use <sys/ucontext.h> on macOS for the in-context unwind
Joshpolansky Jun 20, 2026
18d84ff
Merge pull request #14 from Joshpolansky/feat/crash-diagnostics
Joshpolansky Jun 23, 2026
bcf88d7
feat(command): implement IFunctionBlock interface for CommandFb and a…
Joshpolansky Jun 27, 2026
a37666b
feat(glaze): add Glaze metadata for function-block bases to enable re…
Joshpolansky Jun 28, 2026
2c4018d
feat(module): make longRunning() optional and improve scheduler handling
Joshpolansky Jun 28, 2026
78d030c
feat(tag-table): enhance indexing for nested glz::meta types in TagTable
Joshpolansky Jun 29, 2026
8e98bd1
Merge branch 'claude/infallible-blackburn-eea2e6' into develop
Joshpolansky Jun 29, 2026
f196b66
feat(diag): add system metrics tracking for memory and CPU usage
Joshpolansky Jun 29, 2026
f0e0596
Merge commit '8e98bd1dbea9d21a16d579ed008817263a88018f' into feat/sys…
Joshpolansky Jun 29, 2026
3dc6a0f
fix(diag): drop defaulted Config arg on SystemMetrics ctor (GCC/Clang)
Joshpolansky Jun 29, 2026
2792094
fix(scheduler): define applyAffinityPolicy on all platforms
Joshpolansky Jun 30, 2026
7781c44
refactor(runtime): split loom_runtime into loom_core + native host
Joshpolansky Jun 30, 2026
bd79709
refactor(scheduler): extract sweepClassOnce; add cooperative tickOnce()
Joshpolansky Jun 30, 2026
550f43b
feat(scheduler): honor per-class periods in cooperative tickOnce()
Joshpolansky Jun 30, 2026
67dc99d
feat(runtime): cooperative mode + Emscripten host (run_wasm)
Joshpolansky Jun 30, 2026
b17f8af
feat(scheduler): guard thread spawns for thread-free (wasm) hosts
Joshpolansky Jun 30, 2026
d9a8e57
build(wasm): build the Emscripten runtime host via CMake + Conan
Joshpolansky Jun 30, 2026
6325cb8
feat(api): transport-agnostic REST dispatch shared by native server +…
Joshpolansky Jun 30, 2026
e126207
feat(wasm): runtime-as-a-service β€” fetch('/api/*') served by the in-b…
Joshpolansky Jun 30, 2026
c3ddf5c
feat(wasm): OPC-UA reflected node read/write + WasmMachine (live useV…
Joshpolansky Jun 30, 2026
38d6a6d
feat(wasm): run the real frontend against the in-browser runtime (?wa…
Joshpolansky Jun 30, 2026
21794af
fix(wasm): build-time/sticky wasm-mode flag (replaces fragile ?wasm q…
Joshpolansky Jun 30, 2026
1910c09
feat(api): migrate GET /api/scheduler/classes to dispatch (Scheduler …
Joshpolansky Jun 30, 2026
a0d0f14
feat(api): server.cpp catch-all delegates to dispatch; migrate faults…
Joshpolansky Jun 30, 2026
04e919e
refactor(api): drop server.cpp's scheduler/classes route + DTOs (disp…
Joshpolansky Jun 30, 2026
4a12414
feat(api): migrate scope routes to dispatch (Scope page on wasm)
Joshpolansky Jun 30, 2026
6683cda
feat(api): migrate GET /api/io-mappings to dispatch (Mappings page on…
Joshpolansky Jun 30, 2026
342f747
fix(api): rename Method::DELETE -> DELETE_ (MSVC C2589 on Windows)
Joshpolansky Jun 30, 2026
d9ab013
feat(wasm): build + boot the bundled demo modules under Emscripten
Joshpolansky Jun 30, 2026
f67ebc9
build(wasm): auto-stage the host + modules into frontend/public/ on e…
Joshpolansky Jun 30, 2026
918f3bb
fix(scheduler): address Copilot review findings on tickOnce()/classLoop
Joshpolansky Jun 30, 2026
33f2f9c
Merge pull request #16 from Joshpolansky/loom-core-split
Joshpolansky Jun 30, 2026
fcdf1d3
feat(api): migrate the write routes into dispatch (full module/scope/…
Joshpolansky Jun 30, 2026
0419aeb
Merge remote-tracking branch 'origin/develop' into wasm-spike
Joshpolansky Jul 1, 2026
aa91762
feat(wasm): real pthread threading for the wasm host (drop cooperativ…
Joshpolansky Jul 1, 2026
3ada98f
feat(scheduler): support both real-thread and cooperative builds from…
Joshpolansky Jul 1, 2026
c6e0968
fix(scheduler): close the last path to the tickOnce()/classLoop() rac…
Joshpolansky Jul 1, 2026
c774e29
feat(sdk): loom_add_module() wasm SIDE_MODULE support + arch-independ…
Joshpolansky Jul 1, 2026
a3e68cd
feat(wasm): add bootFromDataDir() manifest-driven browser boot
Joshpolansky Jul 1, 2026
5e21233
feat(wasm): package @joshpolansky/loom-wasm for npm distribution
Joshpolansky Jul 1, 2026
ead4f6e
fix(wasm): drop unnecessary @loupeteam/lux-connect dep, stale ts-expe…
Joshpolansky Jul 2, 2026
919bf55
fix(wasm): address PR #17 review findings (leak, error JSON, detail s…
Joshpolansky Jul 2, 2026
d3342ea
fix(wasm): address PR #18 review findings (leak, tick spam, detail sh…
Joshpolansky Jul 2, 2026
6d7acdf
fix(wasm): address round-2 review findings on PR #17
Joshpolansky Jul 2, 2026
98d28c8
Merge wasm-spike into wasm-pthread (resolve PR #18 base conflicts)
Joshpolansky Jul 2, 2026
0f5a7ee
Merge pull request #17 from Joshpolansky/wasm-spike
Joshpolansky Jul 2, 2026
93d87bf
Merge pull request #18 from Joshpolansky/wasm-pthread
Joshpolansky Jul 2, 2026
f46c0ad
Merge origin/develop into feat/system-metrics (post-wasm integration)
Joshpolansky Jul 2, 2026
f8348c2
fix(diag): address PR #19 review findings
Joshpolansky Jul 2, 2026
5369357
Merge pull request #19 from Joshpolansky/feat/system-metrics
Joshpolansky Jul 2, 2026
bbefbba
Merge branch 'develop' into agents/merge-develop-into-main-release-prep
Joshpolansky Jul 2, 2026
5917c5f
docs: add v0.3.0 changelog entry for develop β†’ main release
Joshpolansky Jul 2, 2026
6577f7c
fix(test): use initGuarded() in TraceCacheTest to open registerExtens…
Joshpolansky Jul 2, 2026
7711549
fix: address PR #21 Copilot review findings before release
Joshpolansky Jul 2, 2026
7d23597
fix(win): include PID in shadow-copy dir to prevent parallel-test races
Joshpolansky Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .github/skills/cruntime-module-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,31 @@ add_subdirectory(<module_name>)
- Fault flags and status

## Key Rules
1. **Aggregate structs only** β€” No constructors, no private members, no virtual methods in data structs
1. **Aggregate data structs** β€” Config/Recipe/Runtime themselves must be aggregates (no constructors, private members, or virtual methods *at the top level*). They MAY contain non-aggregate members β€” function blocks, `loom::Sequence`, any class with glaze metadata β€” as long as each such member is reflectable (see [Reflection & Debuggability](#reflection--debuggability-expose-everything-by-design)). Default *member* initializers are fine, and are required for members with no default constructor.
2. **Default initializers** β€” Always provide sensible defaults
3. **No raw pointers** in data structs β€” use `std::string`, `std::vector`, `std::optional`
3. **No raw pointers** in data structs β€” use `std::string`, `std::vector`, `std::optional`. A reflectable member may *internally* hold handles/pointers (a `CommandClient`, a status `shared_ptr`); that's fine as long as its metadata omits them.
4. **Keep cyclic() fast** β€” No blocking I/O, no allocations, no exceptions
5. **Use longRunning() for async work** β€” File I/O, network, heavy computation
6. **Unique module names** β€” The `name` field in `moduleHeader()` must be unique across all modules
7. **Prefer typed bus helpers** β€” `publishLocal()`, `subscribeTo()`, `registerLocalService()`, and typed `callService()` automatically serialize and deserialize aggregate payloads via glaze
8. **Namespace Bus addresses** β€” Use `publishLocal()`, `registerLocalService()` etc. which auto-prefix with the module instance ID. Use `bus_->publish()`/`bus_->subscribe()` for global topics.

## Reflection & Debuggability (expose everything by design)

Every Config/Recipe/Runtime field is reflected, and the IDE lets a developer **read and write** all of them. This is intentional β€” reflection is for **debuggability**, not just the HMI (the HMI comes for free on top). A developer can set a function block's `execute`, flip an `enable`, or nudge a setpoint live to drive/step a module while developing. **Expose everything; don't hide internal state to "protect" it** β€” the open surface is the point.

So **put framework objects directly in the Runtime** instead of hand-mirroring their state β€” e.g. a `loom::Sequence<Step>` and the PLCopen `MC_*` function blocks dropped straight into the Runtime struct reflect their own live I/O (step/elapsed, execute/busy/done/error), with no mirror fields.

**Make a custom type reflectable** β€” it needs glaze metadata, one of:
- **External** `glz::meta<T>` in a separate `*_glaze.h` (keeps the core control header glaze-free β€” `command.h` itself stays glaze-free; the opt-in metadata lives in `<loom/command_glaze.h>`). Function blocks reuse the base field-list macros β€” `LOOM_COMMAND_FB_FIELDS(T)` / `LOOM_ENABLE_FB_FIELDS(T)` / `LOOM_IFUNCTION_BLOCK_FIELDS(T)` β€” then add their own fields.
- **In-class** `struct glaze { static constexpr auto value = glz::object(...); };` for types you own (it can reach **private** members). Declare it **after** the data members β€” a static-member initializer isn't a complete-class context, so earlier-declared members aren't visible yet.

**Gotchas (these bit us β€” do not repeat):**
- **Reflect only real, assignable data members.** Do *not* use value-returning getter lambdas or member-function pointers in meta: member-function pointers **silently serialize nothing**, and a value-returning getter makes `glz::read<Runtime>` **fail to compile** β€” and the SDK deserializes the *whole* Runtime, so one bad field breaks the entire module's build. To expose a private/computed value, use an in-class `struct glaze` with **data-member pointers**.
- **Enums serialize as integers** unless you provide `glz::meta<E>` with `glz::enumerate("name", E::Value, ...)` for readable names.
- **`glz::merge` is not for base-class reuse** β€” it tries to serialize the member pointers themselves. Use the field-list macros instead.
- **Internal handles are simply omitted** from the metadata β€” list only the fields worth seeing.

## Inter-Module Communication

The `bus_` pointer is injected before `init()`. Three patterns:
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ jobs:
exit 1
fi

# Debug symbols next to the loom binary, so crash reports symbolize
# in-process on the downloaded runtime. Windows: loom.pdb (installed to
# install/bin). macOS: produce loom.dSYM via dsymutil (the .o debug map
# is still present in this job). Linux: DWARF is embedded in the binary.
cp install/bin/loom.pdb "${STAGE}/" 2>/dev/null || true
if [ "${{ runner.os }}" = "macOS" ]; then
shopt -s nullglob
for b in "${STAGE}"/loom "${STAGE}"/loom.exe; do
[ -f "$b" ] && dsymutil "$b" -o "${b}.dSYM" || true
done
fi

# Module plugins. On Windows they are under output/modules/<Config>
# (Debug or Release). On other platforms they are flat in output/modules.
BUILD_TYPE="${{ matrix.build_type }}"
Expand All @@ -191,6 +203,15 @@ jobs:
shopt -s nullglob
for mod in "${MOD_DIR}"/*.so "${MOD_DIR}"/*.dylib "${MOD_DIR}"/*.dll; do
cp "$mod" "${STAGE}/modules/"
# Ship each module's debug symbols alongside it so a crash in
# module code resolves to file:line. Windows: <name>.pdb lives in
# output/ (CMAKE_PDB_OUTPUT_DIRECTORY). macOS: dsymutil β†’ .dSYM.
# Linux: DWARF is embedded in the .so.
base="$(basename "$mod")"; stem="${base%.*}"
[ -f "output/${stem}.pdb" ] && cp "output/${stem}.pdb" "${STAGE}/modules/" || true
if [ "${{ runner.os }}" = "macOS" ]; then
dsymutil "${STAGE}/modules/${base}" -o "${STAGE}/modules/${base}.dSYM" || true
fi
done
fi

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ Thumbs.db
# Frontend
frontend/node_modules/
frontend/dist/
_crashtest/
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,70 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [0.3.0] - 2026-07-02

### Added

#### WebAssembly / Browser Runtime
- **Runtime-as-a-service in the browser**: the full Loom runtime now compiles to WebAssembly via Emscripten and runs directly inside the browser tab; `fetch('/api/*')` calls are intercepted and served by the in-process runtime with no network round-trip
- **Real pthread threading for the WASM host**: replaced cooperative `tickOnce()` polling with real Worker-backed threads (requires `SharedArrayBuffer`/COOP+COEP headers); the scheduler and module `cyclic()` callbacks run on genuine background threads in the browser
- **`bootFromDataDir()`**: manifest-driven browser boot that fetches a `manifest.json`, loads listed `.wasm` side-modules, and instantiates all declared modules β€” enabling self-contained browser demos
- **`@joshpolansky/loom-wasm` npm package**: the WASM host, TypeScript bindings (`WasmMachine`, `LoomRuntime`), and type declarations are packaged for npm distribution; `bootWasmMachine()` is the public entry point
- **OPC-UA reflected node read/write**: `loom_read_node`/`loom_write_node` exported C entry points allow JavaScript to read and write any reflected tag by dotted path; `useVariable()` React hook provides live subscription with glaze round-trip
- **Emscripten CMake + Conan build**: `LOOM_WASM` CMake option builds the WASM host; demo modules compile as `SIDE_MODULE` via `loom_add_module()`; build auto-stages artifacts into `frontend/public/` for Vite dev server
- **WASM-mode frontend flag**: `VITE_WASM` build-time env var (sticky, no fragile query-param) switches the frontend to route all API calls through the in-browser runtime

#### Transport-Agnostic API Dispatch
- **`loom::dispatch()`**: a transport-free REST handler shared by the native Crow server and the WASM in-browser path; accepts `Request{method, path, body}` and returns `Response{status, body}` with no HTTP library dependency
- All REST routes migrated: scheduler/classes, scope, IO mappings, faults, bus topics/services, module detail/data/reload, system metrics, and all write (POST/PATCH/DELETE) routes now live exclusively in dispatch; `server.cpp` is a thin Crow adapter

#### SDK β€” Command Framework (PLCopen-style)
- **`loom::CommandFb<T>`**: PLCopen-compliant function-block base with full Execute/Busy/Done/Error output semantics, rising-edge execute detection, and one-cycle terminal visibility
- **`loom::EnableFb`**: companion always-on function block base with `IFunctionBlock` interface for uniform reflection
- **`loom::CommandClient`**: async command submission returning a `shared_ptr<CommandStatus>` handle; callers poll or await `phase` transitions without blocking cyclic code
- **`loom::CommandChannel`**: per-module typed pending-command queue backed by `RuntimeHeap`; thread-safe submit/consume with mutex-protected `std::vector`
- **`loom::RuntimeHeap`**: cross-module shared-object allocator using `std::shared_ptr` aliasing constructor; objects live in heap-allocated memory visible across `.so` boundaries without a shared allocator ABI

#### SDK β€” Port / Connection Points
- **`loom::Port<T>` / `loom::PortRef<T>`**: typed connection points with generation-counter validation; `PortRef` detects stale connections and self-corrects on next use; `connect()`/`disconnect()` are thread-safe via mutex + generation increment

#### SDK β€” Glaze / Reflection
- **`IFunctionBlock` Glaze metadata**: `glz::meta` specialisation for the `IFunctionBlock` base enables automatic reflection of `execute`, `busy`, `done`, `error`, `errorId` without macros
- **TagTable nested `glz::meta` indexing**: `TagTable` now correctly traverses types that expose a `glz::meta` specialisation (non-aggregate classes) in addition to plain aggregates; the `i++` counter aligns with `glz::reflect<T>::keys` for both

#### Diagnostics
- **Crash diagnostics**: `SIGSEGV`/`SIGABRT`/`SIGFPE`/`SIGILL` are caught by a POSIX signal handler; raw stack addresses are written async-signal-safely to `loom-crash-<id>.txt` before symbolization
- **cpptrace symbolization**: after the raw capture, `cpptrace` symbolizes addresses against `.dSYM` / DWARF debug info and writes a structured JSON report (`loom-crash-<id>.json`) with function name, file, line, and column
- **`/api/faults` + `loom/faults` bus topic**: structured `FaultReport` objects are published to the bus and exposed over REST; the frontend can display crash history without file system access
- **System metrics** (`SystemMetrics` module): tracks RSS memory (peak and current) and per-core CPU utilization at a configurable sample interval; data is reflected as normal runtime fields and visible on the dashboard

#### Scheduler
- **Cooperative `tickOnce()`**: single-call sweep across all classes for the WASM / thread-free host; `sweepClassOnce()` honours each class's individual period using `steady_clock` deadlines; missed-deadline re-anchoring prevents accumulated lag
- **`LOOM_HAS_THREADS` flag**: `thread_support.h` distinguishes Emscripten+pthreads (real Workers) from Emscripten without pthreads (cooperative-only) β€” finer-grained than a raw `#ifdef EMSCRIPTEN` guard
- **`applyAffinityPolicy` stub**: no-op stub provided for all non-Linux/non-Darwin targets (WASM, Windows); closes a missing-symbol link error on those platforms
- **`longRunning()` now optional**: modules that don't override `longRunning()` are detected at `init()` time; the scheduler skips spawning a background thread for them

### Changed
- `loom_runtime` CMake target split into `loom_core` (transport-free logic, `dispatch()`, data engine, scheduler, bus, module loader) and `loom_host_native` (Crow HTTP server, main entry point); downstream modules and tests link `loom_core`
- `loom_add_module()` CMake helper accepts `WASM` keyword to compile a module as an Emscripten `SIDE_MODULE` with the correct link flags; the same `CMakeLists.txt` builds both native `.so` and WASM `.wasm` targets
- `sdk/loom-config.cmake.in` updated to export `loom_core` instead of `loom_runtime`; downstream `find_package(loom)` consumers must link `loom::loom_core`

### Fixed
- **Scheduler tickOnce/classLoop race**: `tickOnce()` now holds `mutex_` for its entire duration, fully serializing cooperative calls against all structural `Scheduler` mutations; the last window for a data race between the cooperative and threaded modes is closed
- **`Method::DELETE_` rename**: `Method::DELETE` renamed to `Method::DELETE_` to avoid MSVC C2589 (`delete` is a reserved keyword in some MS headers); all switch cases and if-guards updated
- **macOS in-context unwind**: crash handler includes `<sys/ucontext.h>` (macOS) instead of `<ucontext.h>` (Linux) for the correct `mcontext_t` definition; the unwind now starts from the faulting instruction frame rather than the signal-handler frame
- **`lastFault` data race**: fault recording uses a release-store of `faulted` after all non-atomic writes; readers use acquire-load, ensuring full visibility of the fault record before observing `faulted == true`
- **`SystemMetrics` GCC/Clang ctor**: removed defaulted `Config` argument from the `SystemMetrics` constructor that triggered a compiler error under GCC/Clang strict-aggregate rules
- **POSIX affinity on all platforms**: `applyAffinityPolicy` now has a definition on every platform that doesn't support `pthread_setaffinity_np`, eliminating a link error on WASM and Windows
- **`.dSYM` CI build rule**: macOS debug-symbol generation moved to a CMake `add_custom_command(TARGET ... POST_BUILD)` dir-level rule so CI doesn't race on parallel directory creation

### Known Issues (non-blocking)
- **`build/Wasm` and `build/Release` share the `conan-release` CMake preset name**: running `just setup-wasm` and `just setup-release` in the same checkout overwrites `CMakeUserPresets.json`; the second `cmake --preset conan-release` uses whichever toolchain ran last. Workaround: run them in separate checkouts or clean between runs. Fix: add `tools.cmake.cmaketoolchain:presets_prefix=wasm-` to `conan/profiles/emscripten`.
- **POSIX signal handler may deadlock on heap-corruption crashes**: the crash handler calls `cpptrace` (which allocates) from the signal path. If the crash was triggered by a corrupted heap, the handler may hang rather than exit. The raw `.txt` report is always written first (async-signal-safe), so no crash data is lost; the process must be `SIGKILL`ed by a watchdog. Fix: defer symbolization to a post-mortem pass.
- **`WasmMachine.dispose()` does not stop the runtime tick timer**: the `setInterval` driving `loom_tick` in cooperative mode keeps running after `dispose()`; call `rt.stop()` manually if tearing down a cooperative-mode instance.
- **`GET /api/modules/:id/data/<badSection>` returns 404 instead of 400**: an unknown section name falls through to the catch-all 404 handler rather than returning a descriptive 400. Acceptable for v0.3.0; fix pending.
- **`lastTickStartMs` always 0 in `GET /api/scheduler/classes`**: the `ClassStatsDto` aggregate initializer is missing its 6th field; the value is correct over WebSocket. Fix pending.

## [0.1.10] - 2026-04-14

### Added
Expand Down
Loading
Loading