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

+21 -8
View File
@@ -137,11 +137,24 @@ def advance_session(session_id: str, action_dict: dict, actor_dict: dict, token:
if terminal_error is not None:
progress_bus.publish(session_id, terminal_error)
elif completed is not None:
repo, dify, actor = completed
view = DifyBuilderService(
repo,
session_lock,
lambda *a, **k: None,
get_app_revision_fn=lambda app_id, owner: dify.read_graph(app_id, owner)[1],
).get_session_view(session_id, actor)
progress_bus.publish(session_id, {"kind": "state", **asdict(view)})
# The advance already succeeded and the lock is released; the session is
# durable. But the view projection (read_graph) or the publish itself can
# still raise -- and if this block let that escape, NO terminal frame
# would reach the SSE client, hanging its stream to the deadline with a
# stale held version (-> 409 on its next action). Fall back to a terminal
# error frame so the client stream always ends.
try:
repo, dify, actor = completed
view = DifyBuilderService(
repo,
session_lock,
lambda *a, **k: None,
get_app_revision_fn=lambda app_id, owner: dify.read_graph(app_id, owner)[1],
).get_session_view(session_id, actor)
progress_bus.publish(session_id, {"kind": "state", **asdict(view)})
except Exception:
logger.exception("dify_builder terminal state projection/publish failed for session %s", session_id)
try:
progress_bus.publish(session_id, {"kind": "error", "error": "step failed"})
except Exception:
logger.exception("dify_builder terminal error frame also failed to publish for session %s", session_id)