Skip to content

Commit 47475c9

Browse files
committed
fix: tear down AG-UI subscription on RUN_FINISHED and RUN_ERROR
The bridge's next handler reacted to RUN_FINISHED / RUN_ERROR by updating flowStore state but did not unsubscribe or resolve the runFlowAGUI promise. Resolution relied on the AG-UI observable completing after the terminal event, which holds only when the SSE stream closes cleanly. A keepalive after RUN_FINISHED, a buffered chunk the reader has not consumed, or a server that does not close eagerly all leave the observable open. The canvas then stays on isBuilding=true and running-status nodes never revert. The fix mirrors the existing teardown in the error: and complete: callbacks: call subscription.unsubscribe() + finish() on the terminal event itself so resolution does not depend on the SSE stream closing. Both branches updated symmetrically since RUN_FINISHED has the same hang risk as RUN_ERROR. No new test scaffold for this fix: reproducing the hang requires a long-lived fake SSE response plus the real flowStore singleton (pulls @xyflow/react and friends), and the fix surface is four lines mirroring the documented complete: pattern. Evidence for the bug comes from the PR #13307 code review and the step 6 code-review pass that re-surfaced the same risk on the success branch. All 29 controllers/API/agui tests still pass.
1 parent aea39cd commit 47475c9

1 file changed

Lines changed: 11 additions & 0 deletions

File tree

src/frontend/src/controllers/API/agui/run-flow-bridge.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,23 @@ export async function runFlowAGUI(opts: WorkflowRunOptions): Promise<void> {
144144
}
145145
} else if (event.type === EventType.RUN_FINISHED) {
146146
flowStore.setBuildInfo({ success: true });
147+
// Tear the subscription down on the terminal event instead of
148+
// waiting for `complete:` from the SSE stream. A server-side
149+
// keepalive or buffered chunk after RUN_FINISHED can leave the
150+
// observable open, which would leave the canvas stuck on
151+
// ``isBuilding=true`` indefinitely.
152+
subscription.unsubscribe();
153+
finish();
147154
} else if (event.type === EventType.RUN_ERROR) {
148155
const message =
149156
(event as unknown as { message?: string }).message ??
150157
"Unknown run error";
151158
flowStore.setBuildInfo({ error: [message], success: false });
152159
setErrorData({ title: "Workflow run failed", list: [message] });
160+
// Same teardown as RUN_FINISHED: a terminal error must not depend
161+
// on the SSE stream closing on its own.
162+
subscription.unsubscribe();
163+
finish();
153164
}
154165
},
155166
error: (err: Error) => {

0 commit comments

Comments
 (0)