Answer node output is not shown or persisted after Human Input resume in Dify 1.15.0
- Dominant language
- TypeScript
- Stars
- 156k
- Forks
- 24.6k
- Avg merge
- 22h 9m
- Merged PRs (30d)
- 610
Description
## Summary
First, I should mention that I am not very familiar with the internal implementation of Dify or graphon.
So my root-cause analysis below may be wrong.
However, while testing workflows with Human Input in Dify 1.15.0, I found a case where the Answer node after resume is executed, but its output is not shown in the app UI and is not persisted as the final assistant message.
From my local reproduction and code inspection, this may be related to `ResponseStreamFilter` state not being preserved across Human Input pause/resume.
If my understanding is incorrect, I apologize. I am opening this issue because the problem may affect other users using HITL workflows.
## Affected version
- Dify: 1.15.0
- graphon: 0.5.x
In my environment, a similar workflow pattern seemed to work in Dify 1.13.0 / 1.14.x.
I have not performed a complete git bisect.
## What happens
When a workflow is paused by a Human Input node and then resumed by submitting the form, the downstream Answer node output may not appear in the app UI.
At the same time, the workflow itself appears to finish successfully.
Observed behavior:
- `node_finished` / `workflow_finished` outputs may contain the expected text
- the workflow status may become `succeeded`
- but the app UI receives no visible Answer message stream
- the final assistant message is empty or not persisted
So internally the workflow looks successful, but the user sees no answer.
## Reproduction patterns
In my environment, I could reproduce the issue with these patterns:
1. An `if-else` node exists upstream of the Human Input node
2. The same workflow run pauses more than once
3. The Answer node after resume references a variable or conversation variable written before the pause
4. Another Answer node was already reached before the pause
A minimal example:
```text
start
-> if-else
-> llm
-> human-input
-> answer
```
After submitting the Human Input form, the workflow resumes and appears to succeed, but the Answer node output is not shown in the app UI.
If I remove the upstream `if-else` node, the same workflow may behave correctly.
## Expected behavior
After resuming from Human Input, the Answer node reached after resume should stream its output normally and persist the final assistant message.
## Actual behavior
After resuming from Human Input, the Answer node output is not shown in the app UI and is not persisted as the final assistant message.
However, the expected text may still exist in workflow/node outputs, so the Answer node itself appears to have been executed.
## My hypothesis
This part is only my hypothesis, and it may be wrong.
In Dify 1.15.0, `iter_dify_graph_engine_events()` wraps `engine.run()` with `ResponseStreamFilter`:
```python
def iter_dify_graph_engine_events(engine):
yield from filter_graph_events(
engine.run(),
context=GraphEventFilterContext.from_engine(engine),
filters=[ResponseStreamFilter()],
)
```
My understanding is that `ResponseStreamFilter` controls when Answer nodes are allowed to stream.
It seems to build a `paths_map` of blocking edges on the path to each Answer node.
When an edge is taken, the filter removes that edge from the corresponding path.
When a path becomes empty, the Answer node becomes eligible for streaming.
However, after a Human Input pause/resume, `iter_dify_graph_engine_events()` creates a new `ResponseStreamFilter()`.
If the filter state from before the pause is not restored, then edges already taken before the pause are treated as not taken in the new filter state. But those edge events will not happen again after resume.
For example, if an upstream `if-else` edge was already taken before the Human Input pause, the resumed filter may wait forever for that edge to be taken again. As a result, the downstream Answer node may never be unlocked for streaming.
This would explain why the problem appears with upstream `if-else`, multiple pauses, variables written before pause, or previous Answer nodes.
## Why I suspect `ResponseStreamFilter` state
I noticed that `ResponseStreamFilter` already seems to have methods for saving and restoring state:
```python
response_filter.dumps()
response_filter.loads(...)
```
The serialized state appears to include fields such as:
```text
paths_map
active_session
waiting_sessions
pending_sessions
stream_buffers
stream_positions
closed_streams
node_execution_ids
```
So it looks like `ResponseStreamFilter` may already be designed to support resume.
However, I could not find where Dify calls `dumps()` before Human Input pause or `loads()` when resuming the workflow.
## Local workaround I tested
I tested a local workaround that does the following:
- save `ResponseStreamFilter.dumps()` when the workflow pauses
- call `ResponseStreamFilter.loads()` when the workflow resumes
With this local patch, the following cases started working again in my environment:
- `if-else` exists upstream of Human Input
- second or later Human Input pause in the same run
- Answer node references a variable written before the pause
- another Answer node was reached before the pause
After applying the workaround, the Answer output was shown in the app UI again and persisted correctly.
I am not sure whether this is the correct production fix.
For an upstream fix, it may be better to store this state in the existing workflow pause/resumption persistence mechanism.
## Notes
I may be misunderstanding the internal design, so please treat the root-cause section as a hypothesis.
But based on my local reproduction, this looks like a pause/resume state restoration issue because:
- it happens in Dify 1.15.0
- the same pattern seemed to work in 1.13.0 / 1.14.x
- the workflow itself can succeed
- node/workflow outputs can contain the expected text
- only the Answer message stream / final assistant message persistence fails
- `ResponseStreamFilter` has `dumps()` / `loads()`, but Dify does not seem to call them around Human Input pause/resume
Please let me know if there is a better minimal reproduction or if I should provide additional logs.
Contributor guide
Research direction
Start at iter_dify_graph_engine_events() and ResponseStreamFilter.dumps()/loads(), then trace the Human Input pause and resume persistence path. Reproduce the listed workflow patterns, especially an upstream if-else, and compare filter state before and after resumption. Done means the resumed Answer output streams in the UI and the final assistant message is persisted.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100