fix(dify_builder): always emit a terminal SSE frame so held version can't go stale

Two paths ended a streaming advance without a terminal `state`/`error`
frame, leaving the SSE client stalled to the deadline with a stale held
version -> its next action 409s:

1. advance_session published the terminal `state` frame outside the
   try/finally and unguarded. If the post-advance view projection
   (get_session_view -> read_graph) or the publish itself raised after a
   SUCCESSFUL advance, the exception escaped and no terminal frame was
   sent. Guard it and fall back to a terminal `error` frame.

2. submit_action_stream emitted a terminal `state` frame on settle only
   for `message`. `update_model` also settles synchronously while bumping
   the version, so it now emits one too -- otherwise the FE keeps the old
   version and 409s on its next action.

Adds regression tests for both (projection-failure -> error frame;
update_model -> state frame).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Charles YaoandClaude Opus 4.8 authored and Wu Tianwei committed 2026-09-03 08:48:23 +08:00
1 parent 87b22412b6
commit de8a5745ea
4 files changed
+86 -9

No files matched your search

+6 -1
View File
@@ -1001,11 +1001,16 @@ class DifyBuilderService:
view, expect_advance = self._prepare_action(session_id, actor, action)
if not expect_advance:
# ``message`` (replay of a settled turn) and ``update_model`` both
# settle synchronously while changing the version, so they must emit
# a terminal ``state`` frame carrying the new version -- else a FE
# tracking its held version off ``state`` frames stays stale and its
# next action 409s.
return stream_advance_frames(
asdict(view),
None,
expect_advance=False,
emit_state_when_settled=action.kind == "message",
emit_state_when_settled=action.kind in {"message", "update_model"},
)
subscription = self._subscribe_fn(session_id) # BEFORE dispatch
try: