Skip to content

fix: drop agent events that arrive while the app shuts down - #477

Closed
hellices wants to merge 1 commit into
mainfrom
fix/476-agent-panel-shutdown
Closed

hellices wants to merge 1 commit into
mainfrom
fix/476-agent-panel-shutdown

Conversation

@hellices

@hellices hellices commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Closes #476

Summary

test_stop_then_immediate_shutdown_injects_cancellation_once failed on test (3.12) in #471's CI with NoMatches: No nodes match '#agent-status' on AgentPanel(), raised out of KorvidApp.on_unmount.

Textual's App._shutdown does three things in order:

  1. Clears _running.
  2. Removes every screen and widget (_close_all).
  3. Only then sends the App its Unmount.

Unmount is where KorvidApp calls begin_shutdown. A turn the user interrupted just before quitting can settle inside step 2. _finish_interrupted_turn then still sees _shutting_down as False and hands TurnInterrupted to the panel port, which looks the panel up on a pruned screen. The NoMatches fails the turn task, shutdown() re-raises it from wait_for_turn, and it escapes app shutdown. A user could hit this by stopping a turn and quitting at once. A loaded runner widens the window.

Verification

tests/ui/test_agent_interrupt.py::test_a_turn_that_settles_after_the_panel_is_pruned_does_not_raise reproduces the ordering deterministically. The interrupted turn's cleanup waits for the panel's Unmount, so it settles after Textual has pruned the panel and before the App's Unmount.

  • RED: without the guard it failed three runs out of three with NoMatches.
  • GREEN: with the guard it passes three runs out of three.

An earlier version that polled for the pruned children every 10 ms passed even without the fix. By the time it noticed, begin_shutdown had already run, which is why the final test waits on the panel's Unmount itself.

Run locally on macOS with Python 3.13 and the frozen lock. Each step ran directly with the project's environment rather than through make, because this machine cannot sync from PyPI:

Command Result
pytest -p no:tach tests/ui/test_agent_interrupt.py tests/ui/test_agent_ui_controller.py tests/ui/test_app_structure.py tests/ui/test_agent_wiring.py 192 passed
python scripts/check_source_size.py passed
ruff check src/ tests/ and ruff format --check src/ tests/ passed; 659 files already formatted
SKIP=mypy,source-size pre-commit run --files <changed files> passed
mypy no issues in 659 source files
tach check all modules validated
pytest -n 8 -q 13,750 passed, 25 skipped

This PR's CI (run 39992085249, hosted runners, four xdist workers) passed every required check:

Job Result
test (3.11) passed, 13m43s
test (3.12) passed, 11m14s
test (3.13) passed, 14m26s
windows-test passed, 9m24s

Checklist

  • This closes an accepted issue in this repository that is assigned to me.
  • Tests cover the change, written to fail first where behavior changed.
  • make check passes locally.
  • User-facing docs and the README match the change, or nothing user-facing changed.
  • No security invariant is weakened: writes still need a keystroke approval
    and a fail-closed audit entry, and sensitive reads are still masked.

Textual's App._shutdown clears _running, removes every screen and widget,
and only then sends the App its Unmount, which is where KorvidApp marks
the agent session down. A turn the user interrupted just before quitting
can settle in between. _finish_interrupted_turn then still sees
_shutting_down as False and hands its TurnInterrupted to the panel port,
which looks the panel up on a pruned screen. The resulting NoMatches
fails the turn task and escapes app shutdown through wait_for_turn
(#476). On #471's CI it surfaced as "No nodes match '#agent-status' on
AgentPanel()".

AppAgentPanel.apply_event, the one place agent events reach the panel,
now drops an event once the app is no longer running. The new test
reproduces the ordering deterministically: the interrupted turn's
cleanup waits for the panel's Unmount, so it settles after Textual
pruned the panel and before the App's Unmount. Without the guard it
fails with NoMatches every run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 17:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

No blocking code issues were identified, but completion of the required Python-version and Windows CI checks remains unconfirmed.

No open issues 🎉

What changed in this PR

Prevents late agent events from updating dismantled UI widgets during shutdown, addressing the NoMatches failure reported in #476.

Changes:

  • Drops agent panel events once Textual marks the app as no longer running, without changing session cleanup.
  • Adds a regression test that makes an interrupted turn settle after the panel is pruned.
File Description
tests/​ui/​test_agent_interrupt.py Adds coverage for an interrupted turn settling during UI teardown.
src/​korvid/​ui/​app_surfaces.py Guards panel event delivery with the app’s running state.

Review effort: Balanced

@my-reviewer-agent my-reviewer-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

검토 범위에서 보고할 이슈가 없습니다.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The narrowly scoped guard addresses the reported race, with targeted regression coverage and no unresolved findings.

No open issues 🎉

Review effort: Balanced

@hellices

Copy link
Copy Markdown
Owner Author

Superseded by #482, which carries this pull request's commits unchanged (d64ad2b) together with the other open fixes and the dependency security bumps, so all of them need one CI run and one merge instead of a rebase after each merge. Closing in favour of #482; the branch stays, so this can be reopened if needed.

@hellices hellices closed this Oct 25, 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.

Agent panel: an interrupted turn that settles during app shutdown raises NoMatches

2 participants