Skip to content

Release v0.3.1 — fix OSC send-failure misclassification, arm CuemsLogger on the library, converge submodules - #8

Merged
backenv merged 17 commits into
mainfrom
integration/v0.3.1
Jul 30, 2026
Merged

Release v0.3.1 — fix OSC send-failure misclassification, arm CuemsLogger on the library, converge submodules#8
backenv merged 17 commits into
mainfrom
integration/v0.3.1

Conversation

@backenv

@backenv backenv commented Jul 30, 2026

Copy link
Copy Markdown
Member

Description

Integration branch for v0.3.1, collecting the T069 fade-killing bug fix, the
library half of the logging backend (PR #7), runner-safe packaging, and two
submodule convergences. integration/v0.3.1main.

The headline item is a production bug: every fade longer than ~25 ms was being
silently killed mid-flight.

Contents

fix: OSC send-failure misclassification (T069) — bedcc3f

liblo's lo_send() returns the number of bytes sent (positive, non-zero) on
success and -1 on failure — never 0. FadeMotion::evalAndSend checked
ret != 0, so it counted every successful send as a failure. Once
kOscFailureThreshold (5 consecutive ticks) was reached the motion was removed,
meaning any fade with duration_ms greater than ~25 ms emitted only a handful of
ticks near start_value and then stopped. Changed to ret < 0.

The test suite missed this because every OscSendFn mock used the opposite
POSIX-style "0 = success" convention; nothing exercised a real liblo-style
positive return. The regression test added here uses a liblo-style mock
(returns 20) and asserts the fade survives past the threshold.

Also documents the OscSendFn return-value contract on both declarations
(FadeMotion.h, MotionRegistry.h) so future callers and mocks can't
reintroduce the assumption, and adds a WARNING on individual send failures
plus a DEBUG when a motion recovers from a transient failure streak — prior
visibility was limited to the terminal MotionError:"osc_send_failed".

Version bumped 0.3.0 → 0.3.1 across CMakeLists.txt, CHANGELOG.md,
debian/changelog.

fix(log): arm CuemsLogger on the gradient_motion library — ec7e812 (PR #7)

HAVE_CUEMS_LOGGER was defined on mtcreceiver and on the gradient-motiond
executable but not on the gradient_motion static library, so every
GME_LOG_* compiled into the library fell back to logging.h's std::cerr
emitter — stamped PRIORITY=6 under systemd and invisible to cuems-logs -e.
GradientEngine.cpp, listed in both targets, compiled twice against two
different logging backends. The daemon-side logging already worked on the fleet;
this fixes the library half.

Arming is now PUBLIC on gradient_motion (guarded by ENABLE_CUEMS_LOGGER AND TARGET cuemslogger, so BUILD_DAEMON=OFF still works), which also removes the
dual-backend hazard. Hand-rolled fprintf(stderr, ...) / cerr sites in
MotionFactory, MotionRegistry, CurveFactory and OscServer were routed
through GME_LOG_*; the two startup-aborting OscServer failures (UDP bind,
lo_server_thread_start) are now GME_LOG_CRITICAL.

⚠️ Reviewer note: with cuemslogger enabled, ctest output now goes to
syslog. That's an intended consequence of PUBLIC arming (test binaries share
the daemon's backend), but it changes local test-run ergonomics.

GradientEngine::onTick (MIDI thread, 100 Hz) was deliberately left untouched —
already level-gated and queue-pop-only.

build(deb): runner-safe packaging — 9f1e0be

The packaging BUILD stage couldn't run reliably on GitHub Actions or any
virtualised runner:

  • dh_auto_test ran the full ctest suite including the two latency-budgeted
    benchmarks, which miss their p99 budgets under neighbour load on shared
    runners; the MTC tests also want an ALSA sequencer a container may not expose,
    and bench_osc_latency writes results back into tests/bench_results/,
    dirtying the source tree mid-package-build.
  • -DBUILD_TESTS=OFF in debian/rules was a dead flag — no such option
    existed in CMakeLists.txt, so every packaging build silently compiled the
    entire test suite.

override_dh_auto_test now skips the suite in the BUILD stage (TEST/COVERAGE
are separate pipeline stages); RUN_DEB_TESTS=1 forces it locally and
DEB_BUILD_OPTIONS=nocheck still wins. override_dh_auto_configure guards for
the submodules and fails up front with the remedy instead of hitting CMake's
FATAL_ERROR late. BUILD_TESTS is now a real option, default ON, so plain
cmake .. and the CI TEST/COVERAGE stages are unaffected.

chore(submodules): converge on reconciled heads — adb7ad0

Both bumps are fast-forwards from what main currently pins:

Submodule main this branch
cuemslogger c39e2d8 6463cd9
mtcreceiver 59fc76e 20ab95b
  • cuemslogger picks up the closelog()-in-setNewSlug memory fix (30c9cb4)
    and the audioplayer header fix.
  • mtcreceiver brings main onto the 24h-continuity line: >24h mtcHead
    continuity (7e9b81d), the resync-tolerance hardening (8a30d05), the
    hold-QF-timebase-on-periodic-full-frame-resync fix (aa44894), plus the
    journald-aware fallback stub and the MTC-RX FullFrame diagnostic at debug
    priority.

This repo builds mtcreceiver with HAVE_CUEMS_LOGGER, so it uses the real
logger path.

docs — e121fd9, 2454c2f, 0285507, 1d4b235, c1ebd03

  • CLAUDE.md restructured, with component detail extracted from the
    cuems-RELATIONS index and the gradient-motiond ↔ engine GradientClient
    OSC contract documented.
  • Stale NNG references dropped. An audit confirmed libnng has been fully gone
    since v0.3.0 (538d992) — no nng_* calls, no find_package(nng), absent
    from debian/control and the CI apt lists — but CLAUDE.md still described
    the daemon as talking over the NNG bus and listed NNG 1.10.1 as an active
    technology. Inbound-transport comments in LockFreeQueue.h, OscServer.h and
    GradientEngine.cpp were generalised rather than merely de-NNG'd, so they
    survive a future transport swap. Historical records (specs/**, CHANGELOG,
    debian/changelog) were deliberately left intact.
  • T069 and T070 planning docs added under specs/planning/. T070 plans the
    layered TEST → COVERAGE → DOCUMENT → BUILD → DEPLOY CI suite and is planning
    only
    — no workflow changes ship in this PR.

ibiltari and others added 17 commits June 11, 2026 12:30
… on start()

>24h MTC continuity baseline. The fade engine consumes the QF tick callback
(MtcTickSource -> callbackMs); the unified receiver makes callbackMs continuous
past 24h so fades keep their phase across the rollover. Add
MtcReceiver::resetWrapOffset() in MtcTickSource::start() before constructing the
receiver, so a tick source restarted in-process after a >24h run starts the new
session at 0 (the wire-driven reset can't catch a graceful restart's small
backward delta). 9/9 unit tests pass incl. test_mtc_tick_source.

Plan 3d.
rc1 + Plan 5 Phase A hardening (strict-< resync tolerance, test-state timeout
reset). API-identical to rc1 (no signature change); the soak ships this commit.
…rame-reset)

Working tree submodule was already checked out at 59fc76e (per CLAUDE.md's
documented pin) but the last commit on this branch had re-pinned the
recorded pointer to 8a30d05, leaving the two out of sync.
Debugging session on the cuems-engine side traced silently-dying fades
back to FadeMotion::evalAndSend treating lo_send()'s bytes-sent return
value as a failure indicator (ret != 0) instead of checking for < 0.
…nd (v0.3.1)

T069 — liblo's lo_send() returns bytes-sent (positive, non-zero) on
success and -1 on failure, never 0. FadeMotion::evalAndSend checked
`ret != 0`, which misclassified every successful send as a failure and
killed any fade with duration_ms > ~25ms once kOscFailureThreshold (5
consecutive ticks) was reached — sending only a handful of ticks near
start_value before removal.

The existing test suite didn't catch this because every OscSendFn mock
used the opposite POSIX-style "0 = success" convention; nothing
exercised a real liblo-style positive return.

- src/motion/FadeMotion.cpp: `ret != 0` -> `ret < 0`.
- src/motion/FadeMotion.h, MotionRegistry.h: document the OscSendFn
  return-value contract to prevent recurrence.
- src/motion/FadeMotion.cpp: WARNING log on individual send failures.
- src/motion/MotionRegistry.cpp: DEBUG log when a motion recovers from
  a transient failure streak.
- tests/test_fade_motion.cpp: regression test with a liblo-style mock
  (returns 20) asserting the fade survives kOscFailureThreshold ticks.
- CMakeLists.txt, debian/changelog, CHANGELOG.md: bump 0.3.0 -> 0.3.1.

See specs/planning/T069-fix-osc-send-failure-misclassification.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NOT MERGED, NOT DEPLOYED — authored on a branch for review at your
convenience (active work is ongoing in this repo). gradient-motiond's
daemon-side logging already works on the fleet (syslog, real priorities);
this fixes the LIBRARY half.

Root cause: HAVE_CUEMS_LOGGER was defined on mtcreceiver and the
gradient-motiond executable but NOT on the gradient_motion static library,
so every GME_LOG_* compiled into the library fell back to logging.h's
std::cerr emitter — stamped PRIORITY=6 under systemd, invisible to
`cuems-logs -e` — and GradientEngine.cpp, listed in BOTH targets, compiled
twice with two different logging backends. That unavailable macro layer is
also why src/motion/* hand-rolled fprintf(stderr, "WARNING ...") lines.

* src/CMakeLists.txt: define + link PUBLIC on gradient_motion, guarded by
  ENABLE_CUEMS_LOGGER AND TARGET cuemslogger (covers BUILD_DAEMON=OFF).
  PUBLIC kills the dual-backend hazard: daemon and the test binaries now
  share one backend. Side effect to be aware of: ctest output goes to
  syslog when cuemslogger is enabled.
* Root CMakeLists: drop the now-redundant PRIVATE arming on the daemon
  (it inherits via the library's usage requirements).
* Hand-rolled fprintf/cerr sites -> GME_LOG_*: MotionFactory (unknown
  curve, lo_address_new failure -> WARNING; START_CROSSFADE stub -> INFO),
  MotionRegistry (crossfade drop -> INFO, cancelMotion miss -> WARNING),
  CurveFactory (unknown curve -> WARNING).
* OscServer: liblo errorHandler + callback exceptions -> GME_LOG_ERROR;
  the two startup-aborting failures (UDP bind, lo_server_thread_start) ->
  GME_LOG_CRITICAL. These bypassed the GME_LOG_* layer the same file uses
  everywhere else.
* GradientEngine::onTick (MIDI thread, 100 Hz) deliberately untouched —
  already level-gated and queue-pop-only.

Verified: full build (daemon + tests), ctest 9/9 passed,
libgradient_motion.a now resolves CuemsLogger symbols.
…eads

cuemslogger c39e2d8 -> 6463cd9: master now carries the anton
closelog()-in-setNewSlug memory fix and the audioplayer header fix
(ancestry-gated before any consumer bumped:
git merge-base --is-ancestor 30c9cb4 6463cd9).

mtcreceiver 8a30d05 -> 20ab95b: same 24h-continuity line this branch's base
already tracks, plus the journald-aware fallback stub (with logDebug and a
shared multi-header guard) and the MTC-RX FullFrame diagnostic at debug
priority. This repo builds mtcreceiver with HAVE_CUEMS_LOGGER, so it uses
the real logger path; run ctest after merging as the base branch intended.
…tion

The packaging BUILD stage could not run reliably on GitHub Actions or any
virtualized runner:

- dh_auto_test ran the full ctest suite, including the latency-budgeted
  test_motion_registry_bench and bench_osc_latency, which miss their p99
  budgets under neighbour load on shared runners. The MTC tests also want
  an ALSA sequencer a container may not expose, and bench_osc_latency
  writes its results back into tests/bench_results/, dirtying the source
  tree mid-package-build.
- -DBUILD_TESTS=OFF in debian/rules was a dead flag: no such option
  existed in CMakeLists.txt, so every packaging build silently compiled
  the entire test suite.
- A checkout without submodules failed late with CMake's FATAL_ERROR
  rather than a legible message.

Changes:

- debian/rules: override_dh_auto_test skips the suite in the BUILD stage
  (TEST/COVERAGE are separate upstream pipeline stages). Escape hatch
  RUN_DEB_TESTS=1 forces it locally; DEB_BUILD_OPTIONS=nocheck still wins.
  override_dh_auto_configure guards for the mtcreceiver/cuemslogger
  submodules and fails up front with the remedy.
- CMakeLists.txt: add a real BUILD_TESTS option, default ON so plain
  `cmake ..` and the CI TEST/COVERAGE stages are unaffected, guarding
  enable_testing() and add_subdirectory(tests).

Verified: default configure still yields 9 tests; -DBUILD_TESTS=OFF
yields 0. Full dpkg-buildpackage -b -us -uc exits 0, compiles no test
binaries, and the extracted binary reports gradient-motiond 0.3.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plans the TEST -> COVERAGE -> DOCUMENT -> BUILD -> DEPLOY pipeline, replacing
the two disconnected workflows (tests.yml, docs.yml) with a gated suite that
also covers the currently-absent BUILD and DEPLOY stages.

Records the repo-specific constraints the implementation must respect, so
they are not re-derived: the two latency-budgeted benchmarks must be collected
rather than gated on; test_motion_registry_bench is mislabelled "integration"
so label-based gating silently does nothing; bench_osc_latency writes into the
source tree; the .deb must be built in a debian:bookworm container because
${shlibs:Depends} otherwise stamps Ubuntu-flavoured dependencies into a package
destined for Debian nodes.

Leaves three decisions open for confirmation: the DEPLOY target (no APT repo
infrastructure exists in the ecosystem, so GitHub Releases is the assumed
artifact of record), the workflow filename (README badges are pinned to
tests.yml), and Pages deploy ownership.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(log): arm CuemsLogger on the gradient_motion library; route stdio bypasses
An audit of the libnng dependency found it is already fully gone: no
nng_* calls or includes, no find_package(nng) in any of the four
CMakeLists, no libnng-dev in debian/control Build-Depends or the CI apt
lists, and `ldd` on the built daemon shows only liblo. It was removed in
538d992 (v0.3.0, spec 007-osc-input-transport), which replaced the NNG
bus client with a localhost UDP OSC listener. libnng is not even
installed on the dev machine and the daemon builds and links fine.

What remained were stale mentions that misdescribe the current design:

- CLAUDE.md still claimed the daemon "talks to the CUEMS engine over the
  NNG bus (libnng)" and listed NNG 1.10.1 as an active technology. This
  is the file seeding every session's context, so it was the highest-value
  fix. Replaced with the OSC transport description and a 007 entry that
  records the supersession explicitly -- a future reader hitting the NNG
  references still in specs/005-* needs to know they are historical.
- CLAUDE.md also said "FadeRegistry map + fixed SPSC status queue". The
  type is MotionRegistry, and the status queue was the NNG-side one that
  was removed; the surviving SPSC queue carries inbound commands.

Comments were generalized rather than merely de-NNG'd, so they survive a
future transport swap:

- src/signal/LockFreeQueue.h: producer is "the inbound command thread"
  (parenthetically, the liblo OSC server thread today), not "the NNG
  receive thread".
- daemon/comms/OscServer.h: "matches NngBusClient convention" referenced
  a deleted file; now "the convention for daemon-specific
  inbound-transport clients".
- src/engine/GradientEngine.cpp: "no NNG" -> "no outbound channel".

Deliberately left as historical record: specs/** (including the 006
quickstart's libnng-dev apt line), .cursor/plans/, CHANGELOG.md,
CONTRIBUTORS.md, debian/changelog, debian/copyright, and the README
release-notes entries. The two test-file comments explaining the local
StatusEvent struct also stay -- that rationale is still live.

Comment-only in the code; the daemon rebuilds and links clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hreshold-t069

Brings in the NNG-reference cleanup (ff44b5e), which belongs with the
CLAUDE.md documentation work on chore/bump-mtcreceiver-24h rather than
with the T069 OSC send-failure fix.
An audit of the libnng dependency found it is already fully gone: no
nng_* calls or includes, no find_package(nng) in any of the four
CMakeLists, no libnng-dev in debian/control Build-Depends or the CI apt
lists, and `ldd` on the built daemon shows only liblo. It was removed in
538d992 (v0.3.0, spec 007-osc-input-transport), which replaced the NNG
bus client with a localhost UDP OSC listener. libnng is not even
installed on the dev machine and the daemon builds and links fine.

What remained were stale mentions that misdescribe the current design:

- CLAUDE.md still claimed the daemon "talks to the CUEMS engine over the
  NNG bus (libnng)" and listed NNG 1.10.1 as an active technology. This
  is the file seeding every session's context, so it was the highest-value
  fix. Replaced with the OSC transport description and a 007 entry that
  records the supersession explicitly -- a future reader hitting the NNG
  references still in specs/005-* needs to know they are historical.
- CLAUDE.md also said "FadeRegistry map + fixed SPSC status queue". The
  type is MotionRegistry, and the status queue was the NNG-side one that
  was removed; the surviving SPSC queue carries inbound commands.

Comments were generalized rather than merely de-NNG'd, so they survive a
future transport swap:

- src/signal/LockFreeQueue.h: producer is "the inbound command thread"
  (parenthetically, the liblo OSC server thread today), not "the NNG
  receive thread".
- daemon/comms/OscServer.h: "matches NngBusClient convention" referenced
  a deleted file; now "the convention for daemon-specific
  inbound-transport clients".
- src/engine/GradientEngine.cpp: "no NNG" -> "no outbound channel".

Deliberately left as historical record: specs/** (including the 006
quickstart's libnng-dev apt line), .cursor/plans/, CHANGELOG.md,
CONTRIBUTORS.md, debian/changelog, debian/copyright, and the README
release-notes entries. The two test-file comments explaining the local
StatusEvent struct also stay -- that rationale is still live.

Comment-only in the code; the daemon rebuilds and links clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…0.3.1

Integrates the T069 work onto the v0.3.1 line:

- fix: OSC send-failure misclassification in FadeMotion::evalAndSend
  (lo_send returns bytes-sent, not 0, on success), plus the 0.3.0 -> 0.3.1
  version bump across CMakeLists, CHANGELOG and debian/changelog.
- build(deb): runner-safe packaging (dh_auto_test skipped in the BUILD
  stage, real BUILD_TESTS option, submodule preflight guard).
- docs(planning): T069 and T070 plans.

Submodule pins resolve to this branch's converged heads (cuemslogger
6463cd9, mtcreceiver 20ab95b) -- the topic branch's re-pin to 59fc76e was
reverted in d9e470d, so it contributes no submodule change.
@backenv backenv self-assigned this Jul 30, 2026
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.69231% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
daemon/comms/OscServer.cpp 0.00% 5 Missing ⚠️
src/motion/MotionFactory.cpp 33.33% 2 Missing ⚠️
src/motion/MotionRegistry.cpp 50.00% 2 Missing ⚠️
src/time/MtcTickSource.cpp 0.00% 1 Missing ⚠️
tests/test_fade_motion.cpp 88.88% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@backenv
backenv merged commit 6cdb874 into main Jul 30, 2026
2 of 4 checks passed
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.

2 participants