#1634·RuView

calibrate-serve: session stuck in `finalizing` forever when duration_s expires with frames < min_frames — no API escape

Author: jkuscevicCreated Aug 18, 2026Updated Sep 16, 2026
Labelsbug

Summary

In calibrate-serve (wifi-densepose-cli), if a calibration session's duration_s expires while frames_recorded is below the requested min_frames, the session enters state: "finalizing" and never leaves it. /status reports finalizing with a growing eta_s indefinitely, POST /stop answers as if no session exists, and no further POST /start can run — the only recovery is restarting the process.

Environment

  • wifi-densepose CLI built from the v2051 tag (crates/wifi-densepose-cli unmodified), calibrate-serve mode, one instance per node behind a UDP fan-out
  • 5× M5Stack Atom S3 Lite (esp32-csi-node) streaming UDP CSI, HT20 tier (52 subcarriers)

Reproduction (deterministic, 5/5 instances)

Request min_frames above what the window can deliver and let duration_s expire:

POST /api/v1/calibration/start {"tier":"ht20","duration_s":300,"min_frames":2400,"room_id":"test"}

At ~3.3 valid HT20 frames/s, 300 s yields ~1000 frames < 2400. Observed on five independent instances simultaneously (same start, same wall-clock):

instance elapsed_s frames_recorded state eta_s (growing)
1 300.10 1042 finalizing 391 → …
2 300.10 996 finalizing 423 → …
3 300.09 955 finalizing 454 → …
4 300.08 1014 finalizing 410 → …
5 300.06 606 finalizing 888 → …

All five stayed in finalizing until the processes were killed. POST /stop on a stuck instance returns the no-active-session response. First hit this on 2026-08-11 with a single instance; today's run reproduced it 5/5.

(Sessions that reach min_frames finalize and persist fine — same setup banked clean baselines minutes earlier with min_frames: 240.)

Mechanism

Three layers line up (paths/lines against v2051):

  1. wifi-densepose-signal/src/ruvsense/calibration.rs:532BaselineRecorder::finalize() correctly returns Err(CalibrationError::InsufficientFrames { got, need }) when frame_count < config.min_frames.

  2. wifi-densepose-cli/src/calibrate_api.rs:605 (async fn finalize) — writes the "finalizing" snapshot before calling the fallible recorder.finalize():

let snap = session_snapshot(&sess, "finalizing", None);
status.write().await.session = Some(snap);

let baseline: BaselineCalibration = sess
    .recorder
    .finalize()
    .map_err(|e| format!("finalize failed: {e}"))?;   // <- error path returns here;
                                                      //    the snapshot is never updated again
  1. calibrate_api.rs:537 (deadline tick) — the guard is a hardcoded floor, not the session's actual target, and the error is discarded:
if Instant::now() >= sess.deadline {
    let frames = sess.recorder.frames_recorded() as usize;
    if frames >= 10 {                                  // <- should be `frames >= target_frames`
        if let Some(done) = active.take() {
            let _ = finalize(done, &output_dir, &status).await;   // <- Err discarded
        }
    } else if let Some(mut done) = active.take() {
        // not enough frames — abort honestly rather than emit a bad baseline
        ...

So with 10 <= frames < min_frames: the tick takes the finalize path, active is already take()n (hence /stop finds nothing), recorder.finalize() errors, the Err is dropped, and the last snapshot anyone can see is "finalizing". The growing eta_s is cosmetic fallout (rate estimate decays while remaining-frames stays constant).

The honest "aborted" path for the < 10 case is exactly the right behavior — it just never runs for this range.

Suggested fix

Either of these alone unsticks it; both together are cheap defense in depth:

  1. In the deadline tick, compare against the session's real target instead of 10, and route the shortfall to the existing abort path with an InsufficientFrames-style note (aborted: only N frames of M required in the time window).
  2. In finalize(), on Err write a terminal snapshot (state: "aborted", note = the error string) instead of leaving "finalizing" behind.

Happy to send a PR for either shape if useful.