Skip to content

fix(flows): close stale-read race in flows_update disarm + actionable-node reachability#4891

Merged
graycyrus merged 1 commit into
tinyhumansai:mainfrom
graycyrus:fix/flows-update-disarm-race
Jul 15, 2026
Merged

fix(flows): close stale-read race in flows_update disarm + actionable-node reachability#4891
graycyrus merged 1 commit into
tinyhumansai:mainfrom
graycyrus:fix/flows-update-disarm-race

Conversation

@graycyrus

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #4889 (merged) addressing two review findings from Codex + CodeRabbit that landed after #4889 was already merged.

Problems & fixes

1. Stale-read race in flows_update disarm (Codex P2 — real concurrency bug). #4889's should_disarm = now_auto && existing.enabled && !was_auto read existing.enabled from an outer get_flow before store::update_flow_graph's own guarded UPDATE re-reads the row. A concurrent flows_set_enabled(id, true) landing in that gap bumps updated_at too — so it wouldn't even trip the optimistic-concurrency conflict, and would silently persist an automatic-trigger graph as enabled: exactly the live-arming bug #4889 was meant to close. Fix: force enabled_override=false unconditionally on any manual/none→automatic transition, decoupled from the stale existing.enabled read (a no-op when the flow was already disabled). The should_disarm messaging is kept separate for log accuracy.

2. graph_has_actionable_nodes reachability (CodeRabbit Major). The predicate used non_trigger_nodes > 0 && !edges.is_empty(), which treats a disconnected component's internal edges as actionable even when unreachable from the trigger. Fix: reachability walk from graph.trigger().

Testing

  • 5 new tests: disconnected-component actionable check, concurrency regression (stale-read race), both enabled_override branches in store_tests.rs.
  • 380 flows tests pass; cargo fmt clean.

Acceptance criteria

  • Race closed — disarm no longer depends on a stale enabled read
  • Reachability-correct actionable-node detection
  • Regression tests for both

…ble-node reachability

- flows_update's manual->automatic disarm decision now forces
  enabled_override=Some(false) unconditionally on the transition, instead of
  gating on the outer `existing.enabled` read. That read is a snapshot taken
  before store::update_flow_graph's own guarded UPDATE re-reads the row; a
  concurrent flows_set_enabled(id, true) landing in the gap bumps updated_at
  too, so it wouldn't even trip the optimistic-concurrency conflict — it
  would silently persist the automatic-trigger graph as enabled (addresses
  @chatgpt-codex-connector on ops.rs:2532).
- graph_has_actionable_nodes now walks reachability from the trigger node
  instead of a naive "any non-trigger node + any edge" count, so a
  disconnected component with its own internal edges no longer suppresses
  the empty-flow note (addresses @coderabbitai on ops.rs:378-384).
- Add regression coverage: disconnected-component empty-flow note, disarm-
  even-when-already-disabled, and a store-level proof that the
  enabled_override always wins over a concurrently-enabled row (addresses
  @coderabbitai on ops_tests.rs:269-302, ops_tests.rs:646-698, and
  store_tests.rs:94-102).
@graycyrus
graycyrus requested a review from a team July 15, 2026 11:39
@graycyrus
graycyrus merged commit 0e86b85 into tinyhumansai:main Jul 15, 2026
17 checks passed
senamakel pushed a commit to M3gA-Mind/openhuman that referenced this pull request Jul 18, 2026
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