fix: going live restores immediately instead of after a few trigger toggles

The symptom was "view a recorded event, click `<live>`, then toggle the trigger a few times before it
starts streaming again". The mechanism was the restore's own gate: `restore_live_state()` waits for the
*stream's* channel list, because the trigger's stored channel is a **name** and a file view that renamed its
channels (`square (A)`) must be gone before the name means the right signal — and that wait was gated on
`generation()` having moved since *Go live* was pressed.

That gate could never be satisfied in the common case. A **single** event keeps the stream's own channel
names, and the restarted client's CONFIG is identical — which the model treats as *no change on purpose*
(`same_stream_signals()`: "the honest recovery is nothing") — so nothing bumped the generation and the
restore sat there. Toggling the trigger *is* a structural change and moved it, which is exactly the
workaround the report describes.

The gate is now the question it was actually asking: is the snapshot's channel name resolvable against the
live channels (or did the snapshot name none)? For a single event that is true as soon as the client has a
channel list at all, so going live is immediate; for a group the names are renamed, so it waits for the
stream's own CONFIG exactly as before — which is what the gate was for. `live_restore_generation` is still
captured and printed (`gen=a/b`) as information, not as the gate.

Verified against the documented reading, both ways into a file view, from a session with a window and a
trigger so the paired fields have something to disagree with:

    [events] live restored (after replay):  window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=6/3
    [events] live restored (after a group): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=16/12

Every want satisfied, `pending=0` in both, `make test-ui` 14 files, zero `Gtk-CRITICAL`. Docs: the
invariants' bullet rewritten around what the restore waits for and why the generation was the wrong
question, and `probe-readings.md` gaining the thing that cost me two runs to find — those four paired
fields are the probe's capture of the live view, so a bare first run reads the application's defaults and
is not a defect; reproduce the documented line from a session that has a window and a trigger.
This commit is contained in:
Martino Ferrari committed 2026-10-14 15:10:49 +02:00
1 parent 8b2dae7655
commit 4e29b958a2
3 files changed
+46 -16

No files matched your search

+18 -4
View File
@@ -3369,10 +3369,24 @@ gboolean tick(gpointer user) {
// window the session file holds, so a restore done before it would be overwritten by the saved
// value. The generation is the other half of the wait -- the stream's first CONFIG, which is what
// gives the stored trigger channel a list to be found in.
if (app->live_restore_pending && !app->replaying && !model.channels().empty() &&
model.generation() != app->live_restore_generation) {
app->live_restore_pending = false;
restore_live_state(*app);
// The wait is for the stream's own channel list, and the question it is really asking is whether the
// trigger's channel *name* can be resolved again: a file view that renamed its channels (`square (A)`)
// has to be gone before the name means the signal it meant. Asking that directly, rather than watching
// `generation()`, is what makes going live immediate in the common case -- a single event keeps the
// stream's own channel names, so its channel list is already the right one, and the generation of a
// restarted client whose CONFIG is identical is *deliberately* unchanged (`same_stream_signals()`), so
// the restore used to wait for something that was never going to happen. What it took to shake loose
// was the trigger toggle, which is a structural change and moved the generation -- the "toggle it a few
// times" this fixes.
if (app->live_restore_pending && !app->replaying && !model.channels().empty()) {
bool resolvable = app->live_state.trigger_channel.empty();
for (const ChannelInfo& c : model.channels()) {
if (c.name == app->live_state.trigger_channel) resolvable = true;
}
if (resolvable) {
app->live_restore_pending = false;
restore_live_state(*app);
}
}
// What the display shows, decided once for the whole frame: the producer can open or