Skip to content

release/0.0.131 - #67

Open
eugenehp wants to merge 1899 commits into
mainfrom
release/0.0.131
Open

eugenehp wants to merge 1899 commits into
mainfrom
release/0.0.131

Conversation

@eugenehp

Copy link
Copy Markdown
Contributor

Summary

  • RLX migration: replaced burn/cubecl/gpu-fft/llama-cpp stack with the RLX inference runtime (rlx, rlx-models, rlx-umap, rlx-fft-metal, rlx-metal) across skill-llm, skill-router, skill-eeg, skill-screenshots, skill-daemon-state, and skill-daemon
  • Dependency cleanup: removed unused cubek-matmul patch (burn-era Metal fix, no longer in crate graph); cleaned up stale deny.toml entries (sleepfm, winreg@0.56.0, cubek.git, tribev2-rs)
  • Clippy / fmt / deny fixes: boxed large enum variant (RlxTextEmbedding), fixed neutts/llm/tts unused-macro warnings, suppressed CLIP float precision lint, removed dead save_user_settings function, fixed doctest (rust,ignore → no_run)
  • Reembed pipeline fix: scan for work before loading the encoder — empty skill_dir now short-circuits to done in <1ms instead of blocking 30s on a HuggingFace download attempt

Test plan

  • cargo clippy --locked --workspace — clean
  • cargo deny check — clean
  • cargo test -p skill-eeg — 120 unit + 5 integration tests pass
  • cargo test -p skill-router --features gpu -- umap_e2e --include-ignored — UMAP Metal E2E (small/medium/large) pass
  • cargo test -p skill-daemon -- routes::settings::tests::trigger_reembed — reembed tests pass in <1s
  • bash scripts/test-daemon-e2e.sh — 19/19 service lifecycle tests pass

🤖 Generated with Claude Code

eugenehp added 30 commits April 23, 2026 10:54
  - Dependabot fixes (rand, atty, crossbeam, memoffset, glib)
  - Grayscale display mode for DND
  - llama-cpp 0.2.50, kittentts 0.4.1
  - Meeting detection (7 platforms)
  - Browser tab extraction (10 browsers)
  - Clipboard monitoring (macOS, with permission UI)
  - Multi-monitor awareness (macOS/Linux/Windows, dynamic resolution)
  - Undo frequency tracking (heuristic + schema for VS Code extension)
  - Productivity scoring, weekly digest, stale file detection
  - ActivityTab dashboard (summary, score, heatmap, top files/projects, languages, sessions, meetings, stale files)
  - Focus session replay with EEG overlay
  - Weekly report with CSV export
  - Weekly digest OS notification (cross-platform)
  - File activity in history sessions + interactive search graph
  - VS Code extension plan document
  - neuroskill CLI updated with 14 new activity subcommands
  - i18n for all features in 9 locales
  - Schema migration for existing databases
  - Retention pruning for all new tables
  - 30+ new Rust tests
  - All vitest/svelte-check/clippy/biome passing
DB schema (4 new tables):
- terminal_commands: shell command text, cwd, exit code, duration,
  auto-categorized (50+ patterns), EEG focus at start + end
- dev_loops: edit→build/test cycle detection with iteration count,
  pass/fail rate, cycle time, focus trend
- zone_switches: editor/terminal/panel transitions with EEG snapshot
- layout_snapshots: periodic tab/group/terminal counts

Event handler:
- terminal_command_start/end → terminal_commands table with categorization
- zone_switch → zone_switches table
- layout_snapshot → layout_snapshots table
- Auto EEG labels for terminal commands and zone switches

Brain endpoints (3 new):
- POST /v1/brain/terminal-impact: focus delta by command category
- POST /v1/brain/context-cost: focus at zone transitions
- POST /v1/brain/dev-loops: edit-build-test cycle analysis

Enhanced analysis:
- predict_struggle: terminal failures (+8/fail, +15/rerun) boost score,
  new suggestions for repeated failing commands
- detect_task_type: terminal categories override heuristics
  (docker→infrastructure, deploy→deploying, git→git_management)
- categorize_command: 50+ patterns across build/test/run/git/docker/
  deploy/install/navigate/debug/network

Updated vscode submodule pointer.
…tore, 284 CLI tools

DB schema:
- conversations table + FTS5 virtual table for full-text search
- eeg_timeseries table (JSON metrics, join-at-query-time)
- embeddings table (generic, multi-model, source-tracked)
- terminal_commands: added binary/args columns for lazy categorization
- ALTER TABLE migrations for existing databases

Analysis (7 developer insights):
- insight_test_failure_by_focus: fail rate by EEG focus level
- insight_hourly_productivity: churn + undo rate + focus by hour
- insight_switch_recovery: focus at zone transitions
- insight_ai_impact: Claude/Pi focus delta vs baseline
- insight_focus_by_language: avg focus + undo rate per language
- insight_loop_efficiency: build/test cycle time by hour
- insight_tool_impact: avg focus per command category
- developer_insights(): all 7 in one call

Endpoints:
- POST /v1/brain/developer-insights
- POST /v1/brain/search-conversations (fts/fuzzy/structured)
- POST /v1/brain/eeg-at, /v1/brain/eeg-range
- POST /v1/brain/binary-stats, /v1/brain/recategorize-commands

Command categorizer: 284 CLI tools across 18 categories
(git, gh, hf, docker, kubectl, aws, brew, pip, psql, ollama, etc.)

EEG attachment: live focus/mood from latest_bands injected at event
storage time for terminal_commands, zone_switches, conversations.

Tauri Activity tab: brain insights section with test failure rates,
focus by language bars, AI impact, tool focus grid, hourly chart.
…s, docs

- CODE_CONTEXT_INDEX_FILE constant for separate code-focused HNSW index
- EEG timeseries worker: writes full band powers every 5s during recording
- ConversationRow Serialize derive fix
- 284 CLI tools categorized (git, gh, hf, docker, brew, aws, ollama, etc.)
- Binary/args extraction + recategorize_commands() for retroactive updates
- binary_usage_stats() for discovering uncategorized tools
- 7 developer_insights() methods (test failures, hourly productivity,
  switch recovery, AI impact, focus by language, loop efficiency, tool impact)
- Tauri Activity tab: Brain Insights section with all 7 insight visualizations
- Updated changelog with brain insights, data architecture, design system docs
- Fix handlers.rs: DaemonTracker no longer has port field (use 18444)
- Fix handlers.rs: create_totp returns IrohTotpView, extract .id
- Fix activity.rs: run_eeg_timeseries takes owned AppState/Arc
- All pre-existing daemon build errors resolved
- Full daemon binary now compiles and runs
- feat-terminal-tracking: shell hooks, VT parser, 284 CLI tools, script recording
- feat-brain-insights: 7 EEG-fused developer insights, timeseries, eeg-at/range
- feat-conversations-search: FTS5/fuzzy/structured search, embedding store, JSONL parsing
- feat-sidebar-cards: 20+ sidebar cards, alerts, daily digest, keyboard shortcuts
- feat-design-system: tokens.css, 7 reusable components, 25 timestamp tests
- feat-dev-loops: cycle detection, enhanced struggle/task-type, loop efficiency
@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.26

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Bugfixes

  • Paired devices survive the BLE backend change: webbluetooth reports Apple device ids as uppercase NSUUID strings where btleplug reported lowercase UUIDs, which would have made every entry in paired_devices.json stop matching after upgrade — the headset would reappear as unpaired and auto-reconnect would never fire. BLE ids are now canonicalised to one spelling (skill_daemon_common::ble_id) at every write, compared case-insensitively at every lookup, and migrated in place on first start.
  • "Forget device" no longer silently fails: the paired-list removal compared ids with exact string equality, so a case difference left the device paired.
  • Duplicate scanner entries: a paired device whose stored id differed in case from the scanner's was pushed into the device list a second time, listing the same headset twice.
  • Daemon can request Bluetooth under launchd: skill-daemon.app's generated Info.plist was missing NSBluetoothAlwaysUsageDescription. Spawned by the Tauri app the outer bundle's key covered it, but started by launchd (RunAtLoad) the daemon is its own responsible process and was denied Bluetooth with no prompt — presenting as a scan that silently found nothing.

Refactor

  • BLE scanner moved to webbluetooth: the daemon's advertisement scanner now runs on the process-wide Bluetooth::shared() session instead of its own btleplug CBCentralManager, so it shares one radio session with muse-rs rather than competing with it. The session coexists with an in-flight connect by design, which removes the need to tear the scanner down and rebuild it around every BLE connect attempt.

Docs

  • docs/webbluetooth-migration.md: what moved to webbluetooth, what is still on btleplug, and what has to happen before the [patch.crates-io] btleplug override can be dropped.

Dependencies

  • muse-rs 0.1 → 0.2: switches the Muse BLE backend from btleplug to webbluetooth. The MuseClient / MuseClientConfig / MuseDevice / MuseHandle / MuseEvent API is unchanged, so the adapter and connect paths needed no edits.
  • webbluetooth 0.0.1 added as a direct dependency of skill-daemon, replacing its direct btleplug dependency. Its backends have no native dependencies — no bluer, no dbus, no windows crates — which drops a runtime libdbus requirement from the Linux packages.

@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.27

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Features

  • fixed windows build

@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.28

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Features

  • updated release windows action

@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.29

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Features

  • MSVC vs LLD

@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.30

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Refactor

  • One CBCentralManager per process, by construction. btleplug is now
    replaced wholesale by patches/btleplug-0.11.9 — its public API
    reimplemented over webbluetooth — via a single [patch.crates-io] entry.
    The five device crates still written against btleplug (awear, idun,
    mendi, mw75, openbci) compile against it unchanged, and every BLE
    operation in the process now funnels through one
    webbluetooth::Bluetooth::shared() session.

    This replaces the eugenehp/btleplug fork, whose only purpose was a macOS
    scanning fix.

    Upstream btleplug's CoreBluetooth backend has no singleton of any kind — no
    OnceLock, OnceCell, lazy_static or static — so every
    Manager::new().adapters() allocated a fresh CBCentralManager on a detached
    thread running loop { wait_for_message().await } with no exit condition.
    Each call permanently leaked a thread, a tokio runtime, a dispatch queue, a
    delegate and a manager. Those calls were not one-time init either: in mw75
    they sit inside scan_all() and connect(), so it was one more manager per
    scan and per connect attempt, and on macOS a second manager cannot discover
    peripherals while another is scanning.

    Migrating the five crates one at a time could not have fixed this — it relies
    on each crate being correct and leaves a second manager live throughout. The
    shim makes a second session unconstructible: Bluetooth::new() and
    with_chooser() appear nowhere in it and have no btleplug concept that maps
    to them. platform::tests::every_adapter_is_the_same_session asserts it.

    src/api/ and src/common/ are byte-identical copies of upstream 0.11.8, so
    the consuming crates see the same types and a future re-sync stays a straight
    copy; only the backend (src/platform.rs) is new. Two places the mapping is
    not 1:1, both handled there: notification fan-in (btleplug hands out one
    merged stream per peripheral, valid before any connection and across
    reconnects, while webbluetooth subscribes per characteristic — so each
    subscribe() spawns a pump that tags values with their characteristic UUID
    into a per-peripheral broadcast channel) and scan refcounting (an
    Adapter is a shared handle, so dropping one must not tear down a scan
    another holder still wants).

  • Remove the Hermes V1 driver. The hermes-ble dependency,
    skill-devices/src/session/hermes.rs, connect_hermes, the
    ConnectRoute::Hermes route and Hermes BLE-name recognition in the scanner
    are all gone.

    hermes-ble was the one crate pinned to btleplug ^0.12.0, which the
    workspace's 0.11 patch could not satisfy — so it resolved unpatched from the
    registry and put a second, entirely separate btleplug stack in the graph. One
    [patch.crates-io] entry supplies one version, so removing it is what let the
    webbluetooth shim cover every remaining btleplug user at once.

    A device paired on an older build still carries its name into routing, and
    select_connect_route ends in a ConnectRoute::Muse catch-all — so a Hermes
    headset would have been silently handed the Muse protocol. Connecting one now
    fails with an explicit message instead, guarded by
    removed_hermes_does_not_fall_back_to_muse.

    Note the Hermes device kind is still listed in the app's supported-devices
    catalogue (DeviceKind::Hermes, its i18n strings and device image), which
    carries no btleplug dependency. Delisting it from the UI is a separate change.

Build

  • Fix the Windows release link failure that broke rc.26 through rc.29. The
    BLE migration pulled in webbluetooth-windows 0.0.1, which declared
    #[link(name = "combase")]; the Windows SDK ships no combase.lib, so every
    Windows build died at the final link of skill-daemon with LNK1181: cannot open input file 'combase.lib'. The WinRT string and activation exports it
    wants (WindowsCreateString, RoGetActivationFactory, and four more) live in
    combase.dll at run time but are imported from runtimeobject.lib. Fixed
    upstream in webbluetooth 0.0.2 and picked up here.

    The earlier SKILL_NO_LLD / forced-link.exe workaround in
    release-windows.yml was treating a symptom — both linkers were correctly
    reporting a library that does not exist — and can be removed separately.

Dependencies

  • Bump webbluetooth 0.0.1 → 0.0.3 and muse-rs 0.2.0 → 0.2.1. Both had to
    move together
    : Cargo treats 0.0.x releases as mutually incompatible, so
    bumping only one resolved to two copies of webbluetooth — which kept the
    unfixed 0.0.1 Windows backend in the graph and meant two
    Bluetooth::shared() OnceLocks, so two CBCentralManagers and devices that
    would not connect. The invariant is now recorded in the workspace root
    Cargo.toml: cargo tree -p skill-daemon -i webbluetooth must print exactly
    one node.

@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.31

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Features

  • fix the session drop issue
  • improve cli

@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.32

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Bugfixes

  • Reconnect could stop silently mid-retry. eval_reconnect_tick waits while
    the device state is "connecting" — correct, since a connect is already in
    flight — but nothing guaranteed that state ever moved on.
    spawn_device_session's failure arm only resets it when still_current holds,
    so a failing attempt whose target had since been rewritten left "connecting"
    behind permanently. The reconnect loop then waited forever: no further
    attempts, and never the give-up branch either, so the log simply went quiet
    with the device stuck. Reported as "gives up after 4 attempts"; the budget is
    12 and was never reached — the loop had stopped asking. The reconnect task now
    detects "connecting" with no live session task for 15 s and treats it as
    disconnected so retrying resumes. See github.com/Muse 2: daemon stays "connected" after headset powers off (stale connection, no reconnect); periodic disconnects after ~8–10 min #89.

  • The daemon and the app could authenticate against different tokens. Path
    resolution for the daemon's config directory existed in four hand-rolled
    copies, and they had diverged: only the app honoured
    SKILL_DAEMON_CONFIG_ROOT, so with that variable set the app read a tmpdir
    auth.token while the daemon read the real one under $HOME — every request
    answering 401 with no way for the UI to drive the daemon. It also meant e2e
    runs that pin daemon paths into a tmpdir silently authenticated against the
    user's real token.

Refactor

  • One definition of the daemon's paths. New skill_daemon_common::paths
    (config_root, token_path) replaces the four copies in
    skill-daemon-state::util, src-tauri/daemon_cmds,
    src-tauri/daemon_upgrade and skill-daemon/main. Each call site keeps its
    own error type and fallback, so behaviour is unchanged where it was already
    correct. Bearer auth compares a secret both processes read from disk, so the
    two resolving different paths cannot degrade gracefully — this is the kind of
    duplication that fails closed.

  • Removed the BLE scan pause. ble_scan_paused, the needs_ble_pause
    predicate, its 400 ms sleep on every BLE connect and the scanner's 300 ms poll
    are gone, along with the AppState field. The hazard was real but is now
    unconstructible: each btleplug crate used to open its own CBCentralManager,
    and on macOS two concurrent scanForPeripherals() calls suppress the
    didConnect: callback so connect() hangs forever. Every BLE consumer now
    goes through one webbluetooth::Bluetooth::shared() session, and one manager
    scanning while it connects is ordinary CoreBluetooth usage.

Build

  • Changelog is readable again. CHANGELOG.md keeps the 20 newest releases
    in full and links the rest to their files under changes/releases/, taking it
    from 6,065 lines / 595 KB to 496 lines / 31 KB. Every release remains
    reachable — verified none missing and none duplicated.

  • The changelog's git-log fallback no longer mislabels everything. When no
    fragment is written, bump derives entries from commit subjects; it filed all
    of them under ### Features regardless of content, which is how "fixed glib
    with a patch" shipped as a feature. Entries are now bucketed by
    conventional-commit prefix, unlabelled commits go to Refactor rather than
    being guessed as features, and the fallback prints a visible warning that raw
    commit subjects are about to become user-facing release notes.

  • Dropped the Windows lld-link workaround. SKILL_NO_LLD and the forced
    link.exe were added on the theory that lld-link could not resolve Windows SDK
    import libraries like combase.lib. combase.lib does not exist in the SDK at
    all, so both linkers were correctly reporting a missing file; the real fault was
    upstream and is fixed. SKILL_NO_LLD was also inert here — only
    tauri-build.js reads it, and the release path uses compile-product.mjs.

  • The Linux .deb declared one dependency and needed fifteen. dpkg-deb --build is a raw packer: unlike rpmbuild, which reads each ELF and emits a
    soname requirement per NEEDED entry, it ships exactly what the control file
    says. The control file said Depends: libopenblas0 and nothing else, so the
    package installed cleanly on a machine missing the entire GTK/WebKit stack and
    the app then failed to start — a worse failure than refusing to install.

    Measured against the rc.31 binaries with dpkg-shlibdeps, the real set is:
    libasound2t64, libc6, libcairo2, libdbus-1-3, libgcc-s1,
    libgdk-pixbuf-2.0-0, libglib2.0-0t64, libgtk-3-0t64,
    libjavascriptcoregtk-4.1-0, libopenblas0, libsoup-3.0-0, libssl3t64,
    libstdc++6, libudev1, libwebkit2gtk-4.1-0 — each with a minimum version.
    Depends is now derived from the three shipped binaries at package time rather
    than hand-maintained, and dpkg-dev is named explicitly in the release apt
    list so a slimmer image cannot quietly lose the tool.

    If dpkg-shlibdeps is missing or yields nothing the script still falls back to
    the old single-dependency value, but now says so loudly on stderr. A silent
    fallback is how this shipped under-declared in the first place: it built, it
    installed, and only the user saw the failure.

  • Removed dead ONNX Runtime plumbing. ort / ort-sys are absent from
    Cargo.lock entirely — KittenTTS was the only consumer and now runs natively
    on RLX — but the build tooling still carried six pieces of machinery for it:
    the .so bundling blocks in package-linux-system-bundles.sh and
    package-linux-dist.sh, an actions/cache step in release-linux.yml
    keyed on a path nothing writes, the unreferenced
    setup-onnxruntime-{linux,windows} composite actions, and
    scripts/install-onnxruntime-linux.sh which only that dead action called. All
    gone, plus the stale ort-sys entries in the standalone
    crates/skill-tts/e2e lockfile, and two comments that still described ORT as
    live on non-Windows targets.

    This also retires a latent silent-exit bug rather than papering over it. The
    lookup was ORT_SO="$(find .../release/build ... | head -1)" under
    set -euo pipefail; find exits non-zero when that directory does not exist,
    so the command substitution killed the whole script with no output at all
    and the graceful "not found" warning below it was unreachable. The release
    always has a build/ directory so it never showed there, but any packaging run
    without one died mute. Deleting the block removes the hazard outright.

  • RPM packaging hardened. The - → ~ rewrite of RC versions is no longer
    written in a form whose meaning depends on the bash version (bare ~ is
    tilde-expanded to $HOME on bash 5; \~ leaves a literal backslash on bash
    3.2), and an allowlist guard now refuses to build rather than let rpmbuild
    silently rewrite an invalid Version. The manual Requires: openblas is
    dropped: rpmbuild already auto-generates libopenblas.so.0()(64bit) from the
    shipped ELF, and the literal package name does not exist on every distro this
    rpm targets.

  • Groundwork for an aarch64 Linux package. Release — Linux builds x86_64
    only, so no ARM Linux .deb/.rpm ships even though
    package-linux-system-bundles.sh already knows the aarch64 arch mapping. The
    release-setup action's apt list is now arch-aware — LunarG publishes
    vulkan-sdk for amd64 only, so a caller can substitute Ubuntu's
    libvulkan-dev libvulkan1 (the same fallback
    scripts/install-vulkan-sdk.sh already uses) and the LunarG repo step is
    skipped when it would contribute nothing. Existing callers pass nothing and
    behave identically.

    A workflow_dispatch-only Trial — Linux arm64 packages workflow builds the
    daemon for aarch64-unknown-linux-gnu, runs the packaging script and asserts
    the resulting rpm: ARCH=aarch64, no - or backslash in Version, a ~
    whenever VERSION is an RC, and an auto-generated libopenblas.so.0
    requirement. It is deliberately not on the tag-driven release path — an
    unproven job there can fail a release, and a second full Linux build doubles
    the cost of every tag — and it uploads to the workflow run rather than to a
    Release, so a trial cannot publish anything.

  • RPM verification harness. npm run verify:rpm (or
    scripts/verify-rpm-docker.sh) installs a NeuroSkill .rpm on a real Fedora
    in Docker and checks it, for x86_64 and aarch64. We build the rpm on Ubuntu
    but ship it to Fedora/RHEL/openSUSE, so the failures that matter are
    distro-side — whether dnf can satisfy the dependencies rpmbuild generated,
    and whether the binary's NEEDED libraries exist under those package names.
    Building it on Ubuntu says nothing about either.

    It takes the package from --rpm, else a local release build, else the newest
    published GitHub release; for an architecture with no real rpm yet (aarch64,
    since Release — Linux is x86_64-only) --synthetic builds one from the same
    spec the release script emits, with a genuinely OpenBLAS-linked binary so the
    dependency and linkage checks still mean something. Checks: name/arch/licence,
    Version legal and tilde-ordered below its release (via rpm.vercmp),
    auto-generated libopenblas.so.0 present and no unsatisfiable bare
    Requires: openblas, dnf install resolves, declared paths exist, rpm -V
    clean, ELF arch matches the package, ldd fully resolves, and removal cleans
    up. Image: Dockerfile.rpm-verify; the checks live in
    scripts/lib/verify-rpm.sh, mounted at run time so editing one needs no
    rebuild.

    The metadata half needs no container: scripts/lib/rpm_meta.py parses the rpm
    header directly (and lists the payload via bsdtar, which macOS ships and which
    understands the rpm container), so version/arch/dependency assertions run on a
    dev box or when the Docker daemon is wedged. The driver probes Docker with a
    bounded wait — docker info blocks forever against a stuck daemon, and macOS
    has no coreutils timeout — then verifies metadata natively and reports the
    install phase as skipped rather than hanging.

    Results on a real Fedora 41, both architectures:

    package result
    shipped rc.31 x86_64 (downloaded from the release) 23 pass, 1 fail — the bare Requires: openblas
    x86_64 via the real packaging script, post-fix 24 pass, 0 fail
    aarch64 via the real packaging script, post-fix 24 pass, 0 fail

    --use-release-script is the mode that makes the last two meaningful: it runs
    scripts/package-linux-system-bundles.sh itself inside the container against a
    stand-in binary, rather than reproducing the spec by hand, so what gets verified
    is the artifact the release pipeline would actually emit. rpmbuild reports
    Requires: /usr/bin/bash libc.so.6()(64bit) libopenblas.so.0()(64bit) rtld(GNU_HASH) — the soname, no bare package name.

    So the defect is demonstrated on the artifact users actually download, and a
    package built from the fixed spec passes every check — including
    dnf install resolving the full dependency set, rpm -V clean, all three
    shipped ELFs (skill, skill-daemon, skill-tty) matching the declared
    architecture with every dynamic library resolving, and a clean erase.
    rpm.vercmp confirms 0.0.131~rc.31 < 0.0.131 on both.

    Two flaws in the harness itself surfaced by running it, both fixed: the image
    has to be built per --platform (building once for the host arch and running
    with --platform linux/amd64 makes docker try to pull a local-only tag), and
    /usr/bin/neuroskill is a bash launcher rather than the binary — checking
    it for an ELF architecture and running ldd against it passed for the wrong
    reason. The checks now inspect the real executables under /opt/neuroskill.

@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.33

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Build

  • Fix the Linux release failing at the dependency step. release-setup's
    packages: is a YAML folded scalar: every line inside it is literal text
    handed to apt, so a leading # does not start a comment. A comment added there
    in rc.32 turned each word of the prose into a package name, and the whole
    Linux release died 3 minutes in with Unable to locate package #,
    dpkg-shlibdeps, used, by, derive, and the rest.

    The explanation now lives above the step, where YAML does treat it as a
    comment, and the block holds package names only — with a note there saying
    exactly this, so the next person does not repeat it. Verified by rendering the
    list straight out of the YAML and resolving every name against real Ubuntu on
    both amd64 and arm64: all 19 resolve, with vulkan-sdk correctly coming from
    the LunarG repo rather than the base archive.

    dpkg-dev (the reason for the comment) stays in the list: it provides
    dpkg-shlibdeps, which derives the .deb Depends from the shipped ELFs.

@eugenehp

eugenehp commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.34

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Build

  • Minor updates and improvements

@eugenehp

Copy link
Copy Markdown
Contributor Author

🚀 New RC: v0.0.131-rc.35

CI is building. Once the workflow finishes, RC channel users will receive this build automatically on their next update check.

Release notes for this RC

Features

  • Oura Ring (Gen 3 / Ring 4 / Ring 5): added support for reading an Oura ring directly over Bluetooth, with no Oura account and no cloud round trip, using the reverse-engineered protocol from open_oura. A connected ring reports live heart rate with its inter-beat interval, the latest cached SpO2, battery state of charge, and — opt-in — accelerometer motion. Discovery, pairing and connection work like any other BLE device.

  • Direct-BLE and cloud Oura are independent: the existing Oura integration syncs sleep, readiness and activity scores from Oura's V2 REST API with a personal access token. The new one talks to the ring itself. Both are configured in Settings → Devices → Device API and neither replaces the other; docs/DEVICES.md lays out which gives you what.

  • Cloud-free Oura history sync (Settings → Devices → Device API → Sync from ring): drains the history the ring itself stores over Bluetooth and writes it to health.sqlite — heart rate, HRV (RMSSD), skin temperature, SpO2, perfusion index, respiratory rate, sleep stages, MET levels and the ring's own detected bedtime window. Incremental: a cursor records where the last sync stopped, and {"reset": true} re-reads everything the ring still holds. Daemon command oura_ring_sync; status via oura_ring_status.

  • Session vitals recording: a recorded session with a vitals device in it now produces an exg_*_vitals sidecar (.csv and/or .parquet, following the session storage format) with timestamp_s, hr_bpm, ibi_ms, spo2_pct, temperature_c. The table is deliberately sparse — a wearable reports one vital at a time, so an absent value is an empty CSV field and a Parquet null, never a zero that would read as a measurement.

  • Device-attributed vitals events: the daemon broadcasts a new Vitals event carrying device_kind and device_id, so heart rate from a ring stays distinguishable from anything else streaming at the same time.

  • Vital Neuro headphones: added support for Vital Neuro EEG headphones over BLE via the vital crate — three EEG channels at 250 Hz, accelerometer and battery telemetry, plus electrode impedance when the device is put in impedance mode. Discovery, pairing and connection work like any other BLE headset, and the headphones appear in Settings → Supported Devices.

  • BLE connect, faster when there is one headset: a Vital Neuro connect stops at the first advertisement when the scanner can see that only one is in range, instead of always running the scan window out to pick by id.

  • BLE discovery by advertised service: the scanner now also identifies a device from the service UUID it advertises, not only its name. Vital Neuro stores its name on the unit and lets the owner change it, so a renamed pair is still discovered and still routed to the right driver.

Performance

  • Local chat uses far less memory: the Qwen3.5 predict graph was compiled twice — once into a cache nothing reads, then again to return — and the chat UI's typing-time warm pass built that graph even though generation never uses it. Both are gone. Metal buffer allocation for the default 0.8B model drops from 25.9 GB to 3.97 GB and peak process memory from ~31 GB to ~11.7 GB.
  • Prefill width is configurable (llm.rlx_prefill_seq): Qwen3.5 runs every prompt through one static prefill graph whose width defaulted to the whole decode context, so a one-line question was prefilled as if it were 8192 tokens. Setting this decouples the two — measured on the 0.8B with a 25-token prompt, time to first token falls from 10.2 s to 1.2 s and decode rises from 5.6 to 35.6 tokens/s. A prompt longer than the configured width is rejected outright, so raise it when tools are on.
  • Agent Skills are no longer injected when they cannot be used: the skills block tells the model to load files with the read_file tool, which is off by default, so 33 discovered skills added roughly 3,900 tokens to every prompt for nothing — a one-line question arrived as 3,970 tokens. It is now sent only when that tool is actually offered, and the daemon says once at startup when skills are being withheld for that reason.

Bugfixes

  • Local chat no longer dies silently mid-request: a panic inside a model kernel unwound the inference actor, so the request's stream just ended — no error to the caller, no status event, and a log that stopped dead after chat request — …. Each request now runs behind a panic boundary: the failure is reported on the request and in the LLM log, and the server keeps serving.
  • Replies containing an accent, CJK or an emoji could kill the actor: the token streamer tracked how much it had sent as a byte offset into the previous decode. Byte-level BPE revises earlier bytes when a codepoint spans tokens — a 3-byte replacement char becomes a 2-byte é — so that offset could land mid-character and the slice panicked. English-only output never tripped it. The cursor is now resynchronised on whole characters.
  • Qwen3 models failed every chat with weight not found: model.embed_tokens.weight: the runner's context was left at the library default of 128 tokens, which sized its decode-bucket ladder to 256. One token past that, decoding fell back to a path that needs F32 weights a packed GGUF does not have. The error named a tensor and looked like a GGUF/safetensors mismatch; it was a context cap. Context is now passed through, as it already was for Qwen3-VL and Qwen3.5.
  • Small models answered with the same phrase over and over: the stop signal a caller returns from its token callback was discarded on the Qwen3 path, so chat EOS, user stop strings and the repetition guard were all ignored and generation always ran to the token cap. Qwen3-0.6B asked for the capital of France produced 303 tokens of "The capital of France's capital of France's …". It also made finish_reason permanently length.
  • Loading a model the catalogue does not describe budgeted memory for the wrong one: context was sized from the active catalogue entry even when a different file was being loaded (a path set directly, a discovered model, --model). Loading a 1-bit 8B while the catalogue still described a 0.8B picked a 16K context for the smaller model and drove the daemon to 16.5 GB resident and 24.6 GB of swap. Mismatches are now detected, warned about, and fall back to a conservative context.
  • max_tokens sent by an OpenAI-compatible client had no effect: it was forwarded under a key nothing reads, so every such request silently ran to the 2048 default.
  • Web search left a panic in the log on every call: the headless renderer tried to create its event loop on a background thread, which macOS forbids, then reported an unrelated timeout before falling back to a plain fetch. It now declines up front with the reason and the supported alternative.

Build

  • npm run daemon builds a GPU-capable daemon by default and takes --model <path> to load a GGUF directly; previously it built without the platform's GPU features, so models ran on CPU, and pointing it at a local file meant four API calls by hand.

UI

  • Live vitals card: a connected vitals device now shows its readings in the dashboard's Physiology view — heart rate, inter-beat interval, SpO2 and skin temperature. Separate from the PPG card, which charts values computed here from an optical waveform: a ring does its own beat detection, so there is no waveform and the PPG card would stay empty. Each tile appears only once a value for it has arrived rather than showing a placeholder zero, and the card explains itself while the device has not measured yet instead of looking broken.

  • A ring no longer shows a Muse head map: the electrode guide gained an Oura tab reporting zero electrodes and no positions, the same treatment Hermes and Ganglion get. Previously any unrecognised device kind fell through to the Muse four-electrode montage.

  • Ring BLE key setting: Settings → Devices → Device API takes the ring's 16-byte app-auth key as 32 hex characters, stored in the OS keychain beside the cloud token. The ring challenges for this key on every connection and answers nothing but device info without it, so connecting without one fails with a message saying where the key comes from rather than producing a session that streams nothing.

  • Electrode guide knows Vital Neuro: connecting one no longer falls back to the Muse head map. The device reports no electrode montage, so the guide names the headset and shows no positions — the same thing it does for Hermes and Ganglion — instead of four electrodes that are not there.

  • Progress is shown while a tool runs: a tool round-trip sent the client nothing at all — 21 of 22 seconds silent on a search — because only generation-phase markers were forwarded. Tool start and finish now appear as phases, so the chat shows "Running tools…" instead of looking stalled.

  • The onboarding model card names the model it will actually download: the title was a fixed string reading "Qwen3.5 4B (Q4_K_M)" in every language while the download was Qwen3.5 0.8B, with the quant badge beside it already showing the truth.

  • Start honours a model picked elsewhere: onboarding ignored the selected model and fetched its own default instead.

  • The LLM panel shows the port the API is really on: it displayed a configurable advertisement value (8375) rather than the port the daemon bound (18444), so the endpoint list and the curl example were both wrong.

Server

  • One BLE session, including the ring: the ring's link is an oura-link Transport implemented directly over webbluetooth, rather than that crate's own btleplug backend. Every radio operation therefore goes through the single process-wide Bluetooth::shared() session the rest of the app uses — on macOS a second session is a second CBCentralManager, and a device found through one cannot be connected through the other. The service-UUID constant is shared between the daemon and the driver, so a dependency graph that ever split webbluetooth in two would fail the build instead of failing at runtime.

  • Discovery by service UUID, for a ring with no name: a bonded Oura ring usually advertises no local name at all, so it is recognised by its vendor service UUID (98ed0001-…) — the same mechanism a renamed Vital Neuro uses. Name matching for the ring now delegates to the device registry, so BLE discovery and connect routing cannot disagree about what counts as a ring.

  • has_vitals capability flag: the session status now distinguishes a device that reports vitals it derived itself from one with a PPG sensor this app reads a waveform from (has_ppg), so the UI can pick the right card instead of inferring from the device kind. Cleared on disconnect with the other capability flags.

  • Ring timestamps are dated properly, or not at all: a history event is stamped with the ring's own counter in deciseconds, which restarts on every reboot — it is not Unix time. skill-oura's new RingClock reconstructs boot epochs and interpolates from time_sync / rtc_beacon anchors (a port of open_oura's epoch_time.py). Events that genuinely cannot be dated — no anchor in their boot, a counter that ran faster than wall time, or lost time spanning several reboots — are dropped and counted rather than stored at the moment they were downloaded, because a wrong date in a health database is worse than a missing row. The sync result reports undated, untrusted and anchors, and the UI explains a sync that read events but could date none of them instead of flashing a success tick.

  • Health conversion refuses unvalidated decodes: only event layouts upstream has confirmed against real captures are stored. Anything carrying "_status": "unvalidated", "inferred" or "part1_raw" is skipped generically, so a future upstream decoder cannot silently start feeding the health database guesses, and every value is range-checked before it is written. ibi_and_amplitude_event is excluded on the same grounds — 7–13 % of its beats read above 100 bpm during sleep on two different rings.

  • A history sync will not fight a live session: the ring accepts one BLE connection, and the protocol matches a response by tag order, so two clients on one link would take each other's answers. A sync refuses with an actionable error while a ring session is connected.

This branch was successfully deployed

1 active deployment
Release — ebec5e21 Deployed Oct 12, 2026 by eugenehp via Build & Release — x86_64-pc-windows-msvc #183
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant