Skip to content

Guard all four PyCoder result/error handlers against a disposed node#94

Merged
dovvnloading merged 1 commit into
mainfrom
hotfix/pycoder-analysis-thread-use-after-dispose
Jul 22, 2026
Merged

Guard all four PyCoder result/error handlers against a disposed node#94
dovvnloading merged 1 commit into
mainfrom
hotfix/pycoder-analysis-thread-use-after-dispose

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Summary

PyCoderNode's result/error handlers were the one worker-result-handler family with no is_disposed guard at all — every sibling (_handle_artifact_result/_handle_artifact_error, _handle_code_sandbox_result/_handle_code_sandbox_error) already had one.

Manual-mode execution finishing kicks off a second, window-level thread (self.pycoder_agent_thread, a PyCoderAgentWorker with no stop() of its own — an in-flight LLM call can't be cancelled early) to get an AI analysis of the output. If the node is deleted mid-execution or mid-analysis, dispose() has no way to reach or stop this second-stage thread — it only knows about the node's own worker_thread, not the window's separate pycoder_agent_thread attribute. When the analysis result (or an error) later arrives, the handler used to mutate, reselect, and save-chat a node no longer on the canvas. The AI-driven mode's single-stage result handler had the identical gap — it wasn't specific to the two-stage manual-mode flow.

Changes

All four handlers now guard on is_disposed, matching the established sibling pattern exactly:

  • _handle_code_execution_result — additionally stops a pointless second-stage analysis thread from ever starting for a node that's already gone
  • _handle_pycoder_analysis_result
  • _handle_ai_pycoder_result
  • _handle_pycoder_error (shared by all three PyCoder worker stages)

Test plan

  • New regression tests for all four handlers (disposed-node and None cases)
  • End-to-end test: delete the node mid-manual-execution, confirm the late finished signal touches nothing and never starts stage 2
  • Full suite: 1703 passed, zero regressions

Fourth of the fixes from the recent adversarial bug-scan (after #91, #92, #93).

PyCoderNode's result/error handlers were the one worker-result-handler
family with no is_disposed guard at all - every sibling
(_handle_artifact_result/_handle_artifact_error, _handle_code_sandbox_result/
_handle_code_sandbox_error) already had one.

Concretely: manual-mode execution finishing kicks off a SECOND, window-level
thread (self.pycoder_agent_thread, a PyCoderAgentWorker with no stop() of
its own - an in-flight LLM call can't be cancelled early) to get an AI
analysis of the output. If the node is deleted mid-execution or mid-analysis,
dispose() has no way to reach or stop this second-stage thread - it only
knows about the node's own worker_thread, not the window's separate
pycoder_agent_thread attribute. When the analysis result (or an error) later
arrives, _handle_pycoder_analysis_result/_handle_pycoder_error used to
mutate, reselect, and save-chat a node no longer on the canvas. The
AI-driven mode's single-stage result handler (_handle_ai_pycoder_result) had
the identical gap - it wasn't specific to the two-stage manual-mode flow.

All four handlers (_handle_code_execution_result, _handle_pycoder_analysis_result,
_handle_ai_pycoder_result, _handle_pycoder_error) now guard on is_disposed,
matching the established sibling pattern exactly. Guarding
_handle_code_execution_result additionally stops a pointless second-stage
analysis thread from ever starting for a node that's already gone.

Full suite 1703 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit a873b1a into main Jul 22, 2026
2 checks passed
@dovvnloading
dovvnloading deleted the hotfix/pycoder-analysis-thread-use-after-dispose branch July 22, 2026 02:08
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