Skip to content

fix(runner+gui): experiment-collision safety (C4/C5) + PWM range from value_spec (B2)#30

Merged
gbradham merged 2 commits into
mainfrom
claude/runner-experiment-safety
Jul 12, 2026
Merged

fix(runner+gui): experiment-collision safety (C4/C5) + PWM range from value_spec (B2)#30
gbradham merged 2 commits into
mainfrom
claude/runner-experiment-safety

Conversation

@gbradham

@gbradham gbradham commented Jul 12, 2026

Copy link
Copy Markdown
Member

Runner function-run safety (review follow-ups C4 + C5)

Follows the device-value-semantics feature (merged, #27#29). The multi-agent review of that feature flagged two latent concurrency risks that only matter now that the Runner "Functions" buttons execute graph functions on demand. This PR closes both.

C5 — a manual run and a real experiment must not overlap

A manual function run briefly forces the flow engine into FlowState.RUNNING to open the exec-propagation gate, and restores the prior state after. A real experiment does the same through SessionState. If they overlap:

  • a manual run in progress makes FlowEngine.start() early-return (state already RUNNING), so a real "Start Experiment" runs its recorders but never runs the flow;
  • a real "Stop Experiment" can be immediately undone by the manual run's finally restoring RUNNING.

Fixes:

  • _run_function_async refuses to run when a real experiment is RUNNING/PAUSED, or when a start/stop is mid-flight — the new GliderCore.is_experiment_busy (experiment-lock held) covers the window where an experiment is starting but state hasn't flipped to RUNNING yet.
  • _start_async refuses to start while a manual function is in flight.

A manual run changes FlowState but never SessionState, so self._core.state cleanly distinguishes a real experiment from a manual run.

C4 — New/Open must actually stop a running function

FlowEngine.clear() (New/Open) cancels _running_tasks, but the runner's body ran in a private asyncio.ensure_future task that was never registered — so clear() couldn't see it, and it kept driving hardware against a torn-down graph until its own 60s timeout.

Fix: add FlowEngine.track_task(); the runner registers its body task, so clear() cancels it. execute() returns a clean False when cancelled out from under it (guards run.cancelled() before run.result()), instead of leaking a CancelledError.

Tests

  • test_main_window_function_run.py — manual run refused during a real experiment and during a start/stop-in-progress; _start_async refused while a function runs.
  • test_flow_function_runner.py — the body task is registered with the engine and an external cancel yields a clean False; track_task lets clear() cancel a registered task.

core+nodes+gui suites green (446 passed).

Residual (not addressed here, single-operator kiosk)

The two guards close the realistic sequential races. A sub-millisecond simultaneous tap of a Function button and Start still has a tiny window (both entry points set their flags inside separately-scheduled async tasks); on a single-operator touchscreen this is negligible and would otherwise need a heavier lock-based redesign.


Also in this branch — B2: PWM controls follow the device value_spec

The same review found two desktop surfaces still hardcoding PWM 0-255 while the node now clamps to the device's real value_spec('set') — so on a 12-bit board (0-4095) neither could author what the node accepts:

  • Graph editor Output-node property panel (node_editor_controller): the PWM spin's range/label now come from the bound device's value_spec('set') (falling back to 0-255 when there is no spec).
  • Desktop DeviceControlPanel: the PWM spinbox/slider range now follows the selected device's value_spec('set') too.

Tests: a 12-bit device yields a 0-4095 control on both surfaces (and a saved value the old cap would have clamped is preserved); no-spec devices fall back to 0-255.

gbradham added 2 commits July 11, 2026 23:08
Two latent concurrency risks surfaced by the device-value-semantics review
(C4/C5), now that the Runner Functions buttons run graph functions.

C5 — a manual function run briefly forces the flow engine RUNNING to gate exec
propagation; a real experiment does the same via SessionState. They must not
overlap or they corrupt each other's engine state (a manual run can make
FlowEngine.start() early-return so a real experiment never runs its flow; a real
stop can be immediately undone by the manual run's state restore). Now:
- _run_function_async refuses when a real experiment is RUNNING/PAUSED or a
  start/stop is mid-flight (new GliderCore.is_experiment_busy covers the window
  where an experiment is starting but state hasn't flipped to RUNNING yet).
- _start_async refuses to start while a manual function is in flight.

C4 — FlowEngine.clear() (New/Open) could not see a running function: the
runner's body ran in a private task never registered with the engine. Add
FlowEngine.track_task(); the runner registers its body task so clear() cancels
it, and execute() returns a clean False when cancelled out from under it rather
than leaking a CancelledError or driving a torn-down graph.

Tests: refusal in both directions + starting-in-progress window; body task is
tracked and an external cancel returns False. core+nodes+gui suites green (446).
The device-value-semantics review found two desktop surfaces still hardcoding
PWM 0-255 while the node now clamps to the device's real value_spec('set') — so
on a 12-bit board (0-4095) neither surface could author what the node accepts.

- Graph editor Output-node property panel (node_editor_controller): the PWM
  spin's range and label now come from the bound device's value_spec('set')
  (falling back to 0-255 when there is no spec), matching what the node writes.
- Desktop DeviceControlPanel: the PWM spinbox/slider range now follows the
  selected device's value_spec('set') too.

Tests: a 12-bit device yields a 0-4095 control on both surfaces (and a saved
value the old cap would have clamped is preserved); no-spec devices fall back
to 0-255. GUI suite green (203).
@gbradham gbradham changed the title fix(runner): keep manual function runs from colliding with experiments (C4/C5) fix(runner+gui): experiment-collision safety (C4/C5) + PWM range from value_spec (B2) Jul 12, 2026
@gbradham gbradham merged commit bc42b0b into main Jul 12, 2026
4 checks passed
@gbradham gbradham deleted the claude/runner-experiment-safety branch July 12, 2026 04:25
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.

1 participant