Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`@ruvnet/rvagent` startup optimization — stdio time-to-first-response ~242 ms → ~189 ms (−22%; MEASURED, median of repeated `initialize` round-trips against `dist/index.js`, this container, reproduce with a piped-stdin timer).** Two changes: (1) `./http-transport.js` is now imported **lazily** inside the `RVAGENT_HTTP_PORT` branch — it chain-loads the MCP SDK's `streamableHttp` module (~48 ms MEASURED via per-module `import()` timing), which the default stdio path never uses; (2) the advertised JSON Schemas generated from the Zod sources are memoized per tool instead of re-walking the Zod tree on every `tools/list` (matters under the session-per-server HTTP model where each session lists tools). No behavior change: 99/99 jest tests, HTTP session flow re-smoke-tested through the lazy path. The `@ruvnet/ruview` harness CLI was profiled too and left alone — 50 ms vs the ~29 ms bare `node -e ''` floor on the same box (MEASURED), i.e. already near the interpreter floor with zero dependencies.

### Fixed
- **Empty room reported "1 person" — the server's raw-CSI classifier overrode the node's calibrated presence.** With CSI capture working (~34 pps of raw `0xC511_0001` frames), the server's raw-CSI motion classifier (`raw_classify` → `smooth_and_classify` → adaptive model) runs per frame and, being **uncalibrated for the specific room**, reads ambient CSI variance (the node's own 50 Hz self-ping traffic, other WiFi, multipath) as `present_moving` — so an empty room the ESP32 correctly reported as empty rendered as occupied, flickering as the raw-CSI updates interleaved with the (correct) feature_state updates. The ESP32 already computes presence on-device against a 60 s empty-room baseline (ADR-081 `feature_state`, `0xC511_0006`), which reads empty=0 correctly. Fix: added `prefer_board_presence()` — when a **fresh** feature_state presence exists (received < 5 s ago; tracked via new `AppStateInner::last_feature_state_at`), it overrides the raw-CSI classifier's `presence`/`motion_level` on both the aggregate and per-node classification (confidence left as the server computed it). The node's calibrated presence now wins; server presence tracks the hardware instead of hallucinating. Pinned by `prefer_board_presence_tests` (3): board-empty overrides a server false-positive (+ confidence untouched), board present-moving/present-still win, and no-fresh-feature_state leaves the server classification intact. `wifi-densepose-sensing-server` bin tests 175 → 178, 0 failed.
- **Sensing server silently dropped a node's default upstream payload — ADR-081 `feature_state` (`0xC511_0006`) was never parsed.** `udp_receiver_task` decoded only raw CSI (`0x01`), edge vitals (`0x02`), fused vitals (`0x04`), and WASM output (`0x07`), but firmware `rv_feature_state.h` documents `feature_state` as **"the default upstream payload from a node ... replaces raw ADR-018 CSI as the primary stream."** A node emitting its default 60-byte compact state (presence/motion/respiration/heartbeat/anomaly/coherence) therefore produced **zero UI updates** — `/health` stuck at `tick:0`, banner OFFLINE — despite frames arriving on UDP :5005 (MEASURED on an ESP32-S3-DevKitC-1: ~5 pps `0xC511_0006` received, all dropped). Added `parse_esp32_feature_state()` which validates the magic + 60-byte length and maps the packet into the existing `Esp32VitalsPacket` (presence fails **closed** on the node's `RV_QFLAG_PRESENCE_VALID` bit AND `presence_score >= 0.5`; `ts_us → timestamp_ms`; `respiration_bpm`/`heartbeat_bpm` passthrough), then wired it as an `.or_else()` fallback after the vitals parser so it flows through the unchanged `SensingUpdate`/WebSocket path. **`presence_score`/`motion_score` are normalised defensively** (non-finite → 0, clamp to `[0,1]`): the firmware (`adaptive_controller.c`) copies a *raw unclamped* detector magnitude into these `0..1`-documented fields — MEASURED at 13.38 on real hardware, which surfaced as "1338%" confidence in the UI — so the server no longer trusts the node to stay in range (the presence/motion booleans still assert from the clamped score). Pinned by `feature_state_parser_tests` (4 tests): field extraction + presence mapping, fail-closed when the valid-flag is clear, wrong-magic rejection, short-buffer rejection. `wifi-densepose-sensing-server` bin tests 171 → 175, 0 failed. (Field note: on a display-less DevKitC the shipped v0.7.0 binary also mis-detects a phantom panel → CSI stays MGMT-only → 0 pps; a display-disabled build restores ~30 pps yield. That is a stale-release/build issue — `main` already carries the RuView#1000 touch-probe bail — not a code change here.)
- **ADR-263/264/265 implemented — the RuView npm surface fixed end-to-end (`@ruvnet/ruview@0.2.0`, `@ruvnet/rvagent@0.2.0`, `@ruv/ruview-cli`).** Harness (ADR-263 O1–O9): `claim-check` now **fails closed** on empty input (CLI exit 2 + `empty_text` tool error); the MCP stdio server dispatches `tools/call` asynchronously over promise-based `spawn` — `ping` answers while a long `verify`/`calibrate` runs (pinned by a new e2e test that runs a 3 s fake proof and asserts sub-second ping); the two `optionalDependencies` are gone so a cold `npx` installs exactly 1 package (MEASURED: was 4 packages / 620 kB / 71 files, `npm i` in a clean prefix); child output is captured as bounded rolling tails (no more 1 MiB `maxBuffer` kills); `node_monitor` passes the port via `sys.argv` instead of splicing it into `python -c` source; the MCP `serverInfo.version` reads package.json; `.claude/skills/*/SKILL.md` are generated from `skills/*.md` by a `prepack` sync script (byte-equality pinned by test); `which()` is a memoized dep-free PATH scan; tools are underscore-canonical (`ruview_claim_check`, …) with the dotted names accepted as call-time aliases, plus `resources/list`/`prompts/list` stubs; the guardrail's `METRIC_TERMS` matching is precision-fixed (word-boundary `map`/`f1`/`auc`/`iou`, code-span + label scrubbing, quantitative-claims-only) — ADR-263/264/265 and both package READMEs now PASS `claim-check` while real untagged claims still flag. 30/30 tests (MEASURED, `node --test`). rvagent (ADR-264 O1–O9): `exports` fixed (types-first, the never-built `dist/index.cjs` `require` target removed — verified broken in the published 0.1.0 tarball); tarball is map-free (127,704 B unpacked / 46 files / 0 maps — MEASURED, `npm pack --dry-run`, down from 188 kB with 44 maps); the Streamable HTTP transport is **actually wired** behind `RVAGENT_HTTP_PORT` with one transport + one MCP server per session (`mcp-session-id` routing), a 1 MiB body cap (413), and a port-aware localhost origin gate — the "dual-transport" description is now true; tools renamed to underscore-canonical with dotted router aliases; ONE Zod validation gate per call with the advertised JSON Schema generated from the same Zod source (`zod-to-json-schema`); `train_count` closes its log fds (was leaking 2/job) and persists job records to `<jobsDir>/<id>.json` so `job_status` survives restarts, with bounded log-tail reads; `detectCogBinary` actually probes its candidate paths; version reads package.json; `@types/express` dropped, `@types/jest` aligned to jest 29; README rewritten to match reality (no phantom `stdio`/`http`/`policy grant` subcommands; unimplemented ADR-124 catalog tools labeled roadmap). 99/99 jest tests (MEASURED); stdio handshake + HTTP session flow + 403/400/404/413 gates smoke-tested live. CLI: bin renamed `ruview-cli` (the `ruview` bin belongs to `@ruvnet/ruview`, ADR-265 D4), version single-sourced. Distribution (ADR-265 D1–D4): new `npm-packages.yml` (3-package × Node 20/22 matrix: tests, version-literal grep gate, pack-content/size gate, tarball-install smoke test incl. the fail-closed claim-check and an ESM-import probe that would have caught the broken `require` export, README claim-check) and `ruview-npm-release.yml` (publish from CI only, `npm publish --provenance`); `ci.yml` NODE_VERSION 18→20.
- **Multistatic fusion never ran on a mixed-mode ESP32 mesh — live bridge fed raw, un-canonicalized per-node CSI to the fuser (#1170).** `node_frame_from_state` (`multistatic_bridge.rs`) wrapped each node's **raw** amplitude vector (HT20 ≈ 64 bins, HT40 ≈ 128/192) into a struct *named* `CanonicalCsiFrame` without ever resampling, so `MultistaticFuser::fuse` tripped `DimensionMismatch` on every cycle, silently fell back to per-node sum/dedup, and spun `total_engine_errors` unbounded. Added `HardwareNormalizer::resample_to_canonical` (resample-only, **no z-score** — preserves the amplitude scale the person-score's `variance/mean²` relies on) and run every node frame through it onto the canonical 56-tone grid before fusion. Heterogeneous meshes now fuse instead of erroring. Pinned by `heterogeneous_node_counts_canonicalize_and_fuse` (mixed 64/192 → fuses), `resample_to_canonical_is_length_only_no_zscore`, and an updated `test_node_frame_conversion`; the pre-existing `engine_bridge::observe_cycle_counts_engine_errors` was retargeted to force a `TimestampMismatch` (its old 56-vs-30 setup now canonicalizes cleanly). `wifi-densepose-signal` 501 / `wifi-densepose-sensing-server` 677 tests, 0 failed.
- **`csi_fps_ema` reported the CSI frame rate 40–840× too high under bursty UDP delivery (#1180).** `update_csi_fps_ema` only rejected deltas `≤ 0` or `≥ 1 s`, so a 36 µs intra-burst arrival delta yielded `1/dt ≈ 27 kHz` straight into the EMA — the metric measured server arrival jitter, not the node's ~40 fps production rate. Added a `MIN_PLAUSIBLE_CSI_DT_SEC = 0.005` floor (derived from the firmware's 50 fps `CSI_MIN_SEND_INTERVAL_US` ceiling, ×4 slack) and made `observe_csi_frame_arrival` keep its anchor across sub-floor bursts so the next genuine inter-frame gap measures true cadence. Pinned by `subms_burst_delta_rejected`, `burst_interleaved_with_nominal_stays_in_band`, and `observe_csi_frame_arrival_ignores_subms_bursts`.
Expand Down
Loading