fix: a clock that stalled on a re-plan, and a dialog that could not close

**The clock.** Trigger mode showed a blank plot and never latched when the session defined a math
channel, because such a session gives the model a plan *before* the stream's own signals are in it:
one derived channel, declared rate 0, so `plan_partition()`'s 1 kHz fallback is the interval those
early frames are stamped at -- 1300x too fast for a 1.3 MS/s stream -- and they have nowhere to go,
since the value loop skips derived channels. Relative mode then stamped a *position* times the plan's
*current* interval, so when the CONFIG's channels landed and the plan took the real rate (769 ns) the
same positions produced times 1300x *smaller* than what was already in the ring, and the monotonic
clamp held the clock at the old value for the rest of the run: one repeated stamp, `history=0`, no
latch, and a window the renderer cannot draw. The same family as the restarted-client poisoning
fixed in 620473a, through a different door. The filter's own output was finite and correct
throughout, and disabling the math channel only appeared to cure it because that is a structural
change which rebuilds the store -- the third time that channel has looked guilty while being a
bystander.

Relative mode now accumulates *seconds* (`rel_base_s_ += n * dt`), so a re-plan is a slope change and
the clock cannot jump backwards by construction; a frame is only stamped into a model that has a
*streamed* channel; and the epoch is adopted once per connection, with "unset" a state rather than
the value zero. A one-shot warning names the family if it returns: `the clock stalled at ...: N
samples stamped [a..b] are all behind it`.

**The dialog.** The Configurations window's Close button and the window-manager close both did
nothing: `close-request` was connected to a bare `void` lambda, and that signal is `gboolean` -- TRUE
means "handled" and suppresses the default handler that destroys the window, while `G_CALLBACK` is a
cast, so GTK read whatever the compiler left in the return register. A standalone GTK program of that
shape reproduces it: `void` handler -> `window_alive=1` after `gtk_window_close()`, `gboolean FALSE`
-> 0. The same audit found `gtk_window_present(GTK_WINDOW(...))` on an `AdwDialog`, a type assertion
and a no-op; it is `adw_dialog_present()` now. The menu row stays: it is the only way to switch or
save a configuration without restarting, and what made it look useless was that it could not close.

Verified: trigger mode with that session's graph reads `ring=[0.0058..0.2446] -> [0.0058..1.9969]`,
`history 0.237 -> 2.0 s`, `edge at 1.120 s, latched at 2.120 s`, `display=event(frozen) latches=1`,
`traceless paints 266` against 1519 before the fix. Baselines hold: 0 errors, the documented three
warnings, test-ui 13 ok, test-help green, `gtk warnings: 0`.
This commit is contained in:
Martino Ferrari committed 2026-10-15 10:50:09 +02:00
1 parent 27c9b34277
commit 074cb611bc
9 files changed
+210 -34

No files matched your search

@@ -197,6 +197,24 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
`process()` uses that for `last_trigger_s_`, `pending_start_s_` **and** the pre-window gate —
all three, or the marker, the cut and the gate would disagree with each other by the width of
a pulse.
- **The trigger's whole input is the stored clock, so a clock that stops is a trigger that never
fires — and *that* is what a blank trigger-mode display is.** `Acquisition` feeds
`TriggerEngine::process(value, rel)` with the same `rel` it stamps the ring with, and the engine
has no other clock: the pre-window gate measures the event against the retained history
(`event_t - oldest_s_ >= pre_seconds_`), the post window counts *stream seconds* past the edge,
and `holdoff_until_s_` is set from `now_s` at the arm. A timeline that stands still therefore
reads as `history=0.0000/1.0000 s` for ever, on every line of `trig engine:`, no matter how many
samples arrive — and with no latch, `display_mode()` never leaves `Waiting`, whose bind is
*nothing* by design, so the plot is blank and the other traces are blank with it (the renderer's
bound window is empty too, so the same emptiness shows with the trigger off). Nothing about that
picture says "clock": it is "in trigger mode, with a math channel enabled, the data disappears",
which is how it reached a user. The reading is the `trig engine:` line's own pair — **`n` climbing
while `ring=[a..a]` stands still** — and the one-shot
`[udpscope] warning: the clock stalled ...` that `on_data()` prints at the first whole frame whose
stamps are all behind the clock. See
[invariants-data-path.md](invariants-data-path.md) for the two entries (a plan whose interval
moved under the stream; a frame accepted into a model with no streamed channel) and for why the
relative clock is accumulated in seconds.
## A latch is also a file