FN-8536: fix planning retry ownership after failure
Release a settled retry token so a later planning error can start the next bounded attempt. - Clear only the matching retry owner before scheduling its successor. - Cover distinct stream errors after retry settlement. - Add a patch changeset for the recovery fix. Files changed: .changeset/fn-8536-planning-retry.md | 7 +++++++ packages/dashboard/app/components/PlanningModeModal.tsx | 10 ++++++++++ .../__tests__/PlanningModeModal.planning-flow.test.tsx | 5 +++++ 3 files changed, 22 insertions(+) Fusion-Task-Id: FN-8536 Fusion-Task-Lineage: cccb0793-390e-4bdb-b459-939760e644bb Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8536-planning-retry.md
Normal file
7
.changeset/fn-8536-planning-retry.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Keep Planning Mode recovery retries safely bounded after failed attempts.
|
||||||
|
category: fix
|
||||||
|
dev: Releases the settled automatic retry owner before scheduling its token-guarded successor.
|
||||||
@@ -1421,6 +1421,16 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
viewRef.current = { type: "loading" };
|
viewRef.current = { type: "loading" };
|
||||||
setView({ type: "loading" });
|
setView({ type: "loading" });
|
||||||
setIsAutoRetrying(true);
|
setIsAutoRetrying(true);
|
||||||
|
/*
|
||||||
|
FNXC:PlanningRetry 2026-07-23-09:45:
|
||||||
|
A rejected automatic attempt has settled before its bounded successor is queued, so
|
||||||
|
release only its matching token first. The successor can then acquire ownership, while
|
||||||
|
duplicate SSE/poll reports still coalesce only during a genuinely pending invocation;
|
||||||
|
a stale callback cannot clear a newer session's owner.
|
||||||
|
*/
|
||||||
|
if (options.retryToken && planningAutoRetryOwnerRef.current?.token === options.retryToken) {
|
||||||
|
planningAutoRetryOwnerRef.current = null;
|
||||||
|
}
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
if (currentSessionIdRef.current === retryTarget.sessionId) {
|
if (currentSessionIdRef.current === retryTarget.sessionId) {
|
||||||
void startPlanningAutoRetryRef.current(retryTarget.sessionId);
|
void startPlanningAutoRetryRef.current(retryTarget.sessionId);
|
||||||
|
|||||||
@@ -166,6 +166,11 @@ describe("PlanningModeModal sequential flow", () => {
|
|||||||
resolveRetry({ success: true });
|
resolveRetry({ success: true });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Settlement releases the first owner. A later, distinct error may acquire the next
|
||||||
|
// bounded attempt; only the duplicate report while the promise was pending is coalesced.
|
||||||
|
mockConnectPlanningStream.mock.calls.at(-1)?.[2]?.onError?.("Later stream error");
|
||||||
|
await waitFor(() => expect(mockRetryPlanningSession).toHaveBeenCalledTimes(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores a stale errored load after a newer session is selected", async () => {
|
it("ignores a stale errored load after a newer session is selected", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user