Skip to content

fix(flow): reset cancellation flag at the start of each execution - #97

Open
XuQuanxin04 wants to merge 2 commits into
wanxingai:mainfrom
XuQuanxin04:fix/flow-cancel-reset
Open

fix(flow): reset cancellation flag at the start of each execution#97
XuQuanxin04 wants to merge 2 commits into
wanxingai:mainfrom
XuQuanxin04:fix/flow-cancel-reset

Conversation

@XuQuanxin04

Copy link
Copy Markdown

Summary

LightFlow.cancel() sets self._cancelled = True, but that flag was only read in _execute() and never reset. Because run(), resume(), and rerun_step() all go through the same _execute() path on a reusable LightFlow instance, a single cancel() call permanently poisoned the instance — every subsequent run skipped all of its steps with "cancelled before execution", even when cancellation was requested after a run had already finished.

Reproduction

flow = LightFlow().step("a", agent=agent_a).step("b", agent=agent_b, depends_on=["a"])
flow.run("one")          # works: a, b succeed
flow.cancel()            # user cancels after run 1 completes
flow.run("two")          # BUG: both a and b are skipped; agent.run() never called

Fix

Reset self._cancelled = False at the start of _execute(). In-flight cancellation is unaffected — the flag is still re-checked before every step, so a cancel() from another thread during a run still skips the remaining steps of that run.

Compatibility

  • Existing agent.run("hello") behavior is unchanged.
  • Existing stream=True behavior is unchanged.
  • In-flight cancel() behavior is preserved (added a test for it).

Tests

  • python -m compileall -q LightAgent
  • PYTHONPATH=. python -m pytest -q tests/test_v065_core.py tests/test_v070_tracing.py tests/test_memory_policy.py — 57 passed
  • Added two tests to tests/test_lightflow.py:
    • test_lightflow_cancel_during_run_skips_remaining_steps_of_that_run — guards the existing in-flight cancel behavior.
    • test_lightflow_cancel_between_runs_does_not_poison_the_next_run — regression test for this bug.
  • Full tests/test_lightflow.py: 16 passed.

cancel() set self._cancelled = True, but that flag was only ever read in
_execute() and never reset. Because run(), resume() and rerun_step() all
share the same _execute() path on a LightFlow instance, a single
cancel() call permanently poisoned the instance: every subsequent run
skipped all of its steps with "cancelled before execution", even when
cancellation was requested after a run had already finished.

Reset the flag when a new execution begins. In-flight cancellation is
unaffected because the flag is still re-checked before every step.

Co-Authored-By: Claude <noreply@anthropic.com>

@wxai-space wxai-space left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested

The underlying bug is valid, but resetting self._cancelled = False at the start of _execute() introduces a cancellation race.

If run() has been called but has not entered _execute() yet, a concurrent cancel() can set the flag and _execute() immediately clears it. The run then executes successfully instead of honoring the cancellation request.

Please replace the shared sticky boolean with an execution-scoped cancellation token or generation captured when run(), resume(), or rerun_step() starts. The token should be checked before each step, and cancellation after an execution has completed should be a no-op for the next run.

Please add a deterministic regression test covering cancellation between invocation of run() and entry into _execute(), plus a test documenting behavior for concurrent executions.

@XuQuanxin04

Copy link
Copy Markdown
Author

Thanks for the review. I replaced the sticky shared boolean with execution-scoped cancellation events registered before run(), resume(), or rerun_step() enters _execute().

cancel() now snapshots and cancels every currently active execution. Calling it after all executions have completed is a no-op for future runs. Each execution checks its own event before every step, and registration is removed in finally paths.

Added deterministic coverage for:

  • cancellation after run() is invoked but before base _execute() begins;
  • cancellation during a run;
  • cancellation after a completed run not affecting the next run;
  • two concurrent executions on one flow (documented behavior: cancel() cancels both active executions).

The public behavior and the recommendation to use separate LightFlow instances for independently cancellable concurrent runs are documented in docs/lightflow.md.

Validation: 265 passed, 1 skipped; repository-required focused suite included; compileall and git diff --check passed.

Updated in 288d692.

@XuQuanxin04

XuQuanxin04 commented Sep 4, 2026 via email

Copy link
Copy Markdown
Author

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.

2 participants