Skip to content

fix(web): keep webapp generation running when switching conversations - #41227

Draft
GareArc wants to merge 3 commits into
mainfrom
fix/webapp-no-stop-on-conversation-switch
Draft

fix(web): keep webapp generation running when switching conversations#41227
GareArc wants to merge 3 commits into
mainfrom
fix/webapp-no-stop-on-conversation-switch

Conversation

@GareArc

@GareArc GareArc commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Sending a long message in a WebApp Advanced Chat and then switching to another conversation while it is still streaming truncates the answer. The message is persisted with status=normal and error=None, so it looks like a complete reply rather than a cancelled one.

The frontend switches conversations fires POST /chat-messages/{task_id}/stop.

web/app/components/base/chat/chat/hooks.ts exposed a single handleStop() that did two unrelated things — released this client's subscriptions (aborting the SSE POST, the conversation-messages and suggested-questions fetches, resetting the workflow-events subscription), and cancelled the run server-side. Both conversation-navigation paths in chat-with-history and embedded-chatbot called it.

That coupling was harmless when generation ran inside the HTTP request, because disconnecting was equivalent to cancelling. It is not harmless now: streaming Advanced Chat executes in a Celery worker and the HTTP request is only one subscriber on the run's Redis topic. Dropping the response no longer cancels anything, but the explicit stop call still does — so navigation became the one action that kills a run implicitly, which is the opposite of what the backend does everywhere else (AppExecutionCoordinator: "Response detachment is not an execution cancellation signal").

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran make lint && make type-check (backend) and vp staged (frontend) to appease the lint gods

Frontend only, so the backend gates do not apply. vp staged has no config in this repo; vp check and tsc both pass on the changed files. Tests: 1441 passing across 78 files in app/components/base/chat and __tests__/base.

Three tests cover this change, deliberately kept narrow:

  • useChat detaches without cancelling the run, and a detached run can no longer be cancelled afterwards.
  • Switching conversation detaches instead of stopping — the reported bug.
  • Deleting the current conversation still stops, so the suite cannot pass on a build that has lost the ability to cancel at all.

Notes for reviewers

  • handleStop is now reached from exactly two places: the stop button, and deleting the conversation a run belongs to. Everything else detaches.
  • The registration effect in chat-with-history/chat-wrapper.tsx gained a real dependency array (it was []). The fix depends on the registered callbacks being current, and this matches what embedded-chatbot already did.
  • Deleting a conversation other than the current one no longer cancels a run that was detached from it. The client cannot do this — the task id is gone once the wrapper unmounts — so it would need a server-side stop on conversation delete. Out of scope here.

@github-actions github-actions Bot added the web This relates to changes on the web. label Aug 25, 2026
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.05556% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.71%. Comparing base (b598292) to head (cfe1c1d).
⚠️ Report is 39 commits behind head on main.

Files with missing lines Patch % Lines
...e/app/apps/advanced_chat/generate_task_pipeline.py 0.00% 3 Missing ⚠️
api/core/app/apps/base_app_queue_manager.py 0.00% 0 Missing and 1 partial ⚠️
api/core/app/apps/execution_coordinator.py 85.71% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #41227      +/-   ##
==========================================
- Coverage   86.82%   86.71%   -0.11%     
==========================================
  Files        5266     5431     +165     
  Lines      302135   307120    +4985     
  Branches    60485    61751    +1266     
==========================================
+ Hits       262314   266323    +4009     
- Misses      34591    35557     +966     
- Partials     5230     5240      +10     
Flag Coverage Δ
api 86.57% <88.09%> (-0.21%) ⬇️
cli 89.34% <ø> (?)
dify-ui 93.75% <ø> (ø)
web 86.77% <100.00%> (-0.06%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Switching to another conversation while a message was still streaming
fired POST /chat-messages/{task_id}/stop, cancelling the run server-side
and leaving a truncated answer persisted as a normal, error-free message.

Streaming execution now runs in a Celery worker and the HTTP request is
only one subscriber on the run's Redis topic, so dropping the response no
longer cancels anything - but the explicit stop call still does. Wiring
navigation to the same handler as the stop button therefore turned a
"stop listening" action into a "stop generating" one.

Split useChat's handleStop in two: handleDetach releases this client's
subscriptions, and handleStop additionally cancels the run. Switching
conversations and starting a new one now detach; the stop button and
deleting the conversation a run belongs to still cancel it.

handleDetach also clears taskIdRef, so a detached run can no longer be
cancelled through a stale chat instance reference after the wrapper has
remounted for another conversation.
@GareArc
GareArc force-pushed the fix/webapp-no-stop-on-conversation-switch branch from 297914a to 99126ca Compare August 25, 2026 05:31
@GareArc
GareArc marked this pull request as ready for review August 25, 2026 05:48
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Aug 25, 2026
@GareArc
GareArc marked this pull request as draft August 25, 2026 05:54
A run the user cancelled and one the watchdog aborted on timeout were both
persisted as ordinary, error-free messages, so the logs reported them as
successes and the webapp gave no sign the answer was cut short.

The abort cause was lost twice. workflow_app_runner hardcoded USER_MANUAL
for every GraphRunAbortedEvent, and the message row was never moved off
`normal` on the stop path - only the workflow run picked up STOPPED.

QueueStopEvent.StopBy now owns its reason text and recovers a member from
the free-form reason that crosses the graph engine boundary, degrading to
UNKNOWN for a value this version has never heard of and passing the
engine's own text through untouched. The coordinator and AppTaskService
send that typed cause, so a timeout is no longer reported as a manual stop.

MessageStatus gains STOPPED, which the advanced chat pipeline persists.
Both log surfaces read it: the console conversation list gained a stopped
count, and the agent observability logs report and filter on it. A failure
still outranks an interruption in the same conversation.

MessageListItem.status and StatusCount.stopped are typed on the API rather
than left as bare str, so the generated contract carries the enum and the
web drops its two hand-written mirrors. The webapp narrows the served
status at the boundary and renders nothing for one it cannot recognise.
@github-actions

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 61.11% 61.11% +0.00%
Strict coverage 60.70% 60.70% +0.00%
Typed symbols 41,861 41,866 +5
Untyped symbols 26,823 26,825 +2
Modules 3245 3245 0

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Diff

base → PR
--- /tmp/pyrefly_base.txt	2026-08-25 09:44:29.198699384 +0000
+++ /tmp/pyrefly_pr.txt	2026-08-25 09:44:21.456660500 +0000
@@ -5368,8 +5368,6 @@
    --> tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py:880:60
 ERROR `(**kwargs: Unknown) -> TestWorkflowGenerateTaskPipeline.test_save_output_for_event_writes_draft_variables._Saver` is not assignable to attribute `_draft_var_saver_factory` with type `DraftVariableSaverFactory` [bad-assignment]
    --> tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py:921:45
-ERROR `Literal['unknown']` is not assignable to attribute `stopped_by` with type `QueueStopEvent.StopBy` [bad-assignment]
-  --> tests/unit_tests/core/app/entities/test_queue_entities.py:18:28
 ERROR Cannot index into `bool` [bad-index]
   --> tests/unit_tests/core/app/entities/test_task_entities.py:32:16
 ERROR Cannot index into `float` [bad-index]
@@ -8318,12 +8316,14 @@
    --> tests/unit_tests/services/agent/test_agent_dsl_service.py:578:17
 ERROR Argument `SimpleNamespace` is not assignable to parameter `workflow` with type `Workflow` in function `services.agent.dsl_service.AgentDslService._create_workflow_only_agent` [bad-argument-type]
    --> tests/unit_tests/services/agent/test_agent_dsl_service.py:601:18
+ERROR Argument `SimpleNamespace` is not assignable to parameter `message` with type `Message` in function `services.agent.observability_service.AgentObservabilityService._message_status` [bad-argument-type]
+   --> tests/unit_tests/services/agent/test_agent_observability_service.py:262:54
 ERROR Argument `SimpleNamespace` is not assignable to parameter `message` with type `Message` in function `services.agent.observability_service.AgentObservabilityService.serialize_log_message` [bad-argument-type]
-   --> tests/unit_tests/services/agent/test_agent_observability_service.py:630:9
+   --> tests/unit_tests/services/agent/test_agent_observability_service.py:638:9
 ERROR Argument `SimpleNamespace` is not assignable to parameter `conversation` with type `Conversation | None` in function `services.agent.observability_service.AgentObservabilityService.serialize_log_message` [bad-argument-type]
-   --> tests/unit_tests/services/agent/test_agent_observability_service.py:631:9
+   --> tests/unit_tests/services/agent/test_agent_observability_service.py:639:9
 ERROR Argument `list[SimpleNamespace]` is not assignable to parameter `feedbacks` with type `Sequence[MessageFeedback]` in function `services.agent.observability_service.AgentObservabilityService.serialize_log_message` [bad-argument-type]
-   --> tests/unit_tests/services/agent/test_agent_observability_service.py:632:9
+   --> tests/unit_tests/services/agent/test_agent_observability_service.py:640:9
 ERROR Argument `SimpleNamespace` is not assignable to parameter `binding` with type `WorkflowAgentNodeBinding` in function `services.agent.composer_service.AgentComposerService._workflow_inline_debug_conversation_id` [bad-argument-type]
    --> tests/unit_tests/services/agent/test_agent_services.py:400:17
 ERROR Argument `SimpleNamespace` is not assignable to parameter `agent` with type `Agent | None` in function `services.agent.composer_service.AgentComposerService._workflow_inline_debug_conversation_id` [bad-argument-type]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files. web This relates to changes on the web.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant