diff --git a/.changeset/fix-planning-session-timers.md b/.changeset/fix-planning-session-timers.md
new file mode 100644
index 0000000000..f2013c3887
--- /dev/null
+++ b/.changeset/fix-planning-session-timers.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Keep planning timers session-specific and return cleanly from stopped generations.
+category: fix
+dev: Persists each generation's start time and restores the prior planning step when stopped.
diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx
index ffdf950b59..5803f43584 100644
--- a/packages/dashboard/app/components/PlanningModeModal.tsx
+++ b/packages/dashboard/app/components/PlanningModeModal.tsx
@@ -811,16 +811,20 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
return;
}
- const startedAt = Date.now();
- setGenerationStartTime(startedAt);
- setElapsedSeconds(0);
+ const startedAt = generationStartTime ?? Date.now();
+ if (generationStartTime === null) {
+ setGenerationStartTime(startedAt);
+ }
- const timer = setInterval(() => {
+ const updateElapsed = () => {
setElapsedSeconds(Math.max(0, Math.floor((Date.now() - startedAt) / 1000)));
- }, 1000);
+ };
+ updateElapsed();
+
+ const timer = setInterval(updateElapsed, 1000);
return () => clearInterval(timer);
- }, [view.type]);
+ }, [generationStartTime, view.type]);
// Fallback for missed SSE 'question'/'summary' events: when the loading
// state lingers, periodically refetch the session and transition the view
@@ -1218,6 +1222,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
setIsRetrying(!options.auto);
setIsAutoRetrying(options.auto);
setStreamingOutput("");
+ setGenerationStartTime(Date.now());
setView({ type: "loading" });
currentSessionIdRef.current = retryTarget.sessionId;
@@ -1366,6 +1371,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
refineSummaryInFlightRef.current = false;
setGenerationActivity("initial_plan");
savePlanningDescription(startedPlan, projectId);
+ setGenerationStartTime(Date.now());
setView({ type: "loading" });
try {
@@ -1499,6 +1505,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
setIsRetrying(false);
setIsRefiningSummary(false);
refineSummaryInFlightRef.current = false;
+ setGenerationStartTime(null);
setView({ type: "loading" });
try {
@@ -1544,6 +1551,14 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
// An unavailable payload cannot provide a safe copy target.
}
setActivePlanPrompt(typeof inputPayload?.initialPlan === "string" ? inputPayload.initialPlan : "");
+ const persistedGenerationStartedAt = typeof inputPayload?.generationStartedAt === "string"
+ ? Date.parse(inputPayload.generationStartedAt)
+ : Number.NaN;
+ setGenerationStartTime(
+ session.status === "generating" && Number.isFinite(persistedGenerationStartedAt)
+ ? persistedGenerationStartedAt
+ : null,
+ );
if (inputPayload?.generationPurpose === "plan_update" || inputPayload?.generationPurpose === "question" || inputPayload?.generationPurpose === "initial_plan") {
setGenerationActivity(inputPayload.generationPurpose);
} else if (session.status === "generating") {
@@ -2250,6 +2265,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
setConversationHistory(optimisticHistory);
resetPlanningAutoRetryBudget();
setGenerationActivity("plan_update");
+ setGenerationStartTime(Date.now());
setView({ type: "loading" });
setStreamingOutput(""); // Clear old thinking output when entering loading state
liveGenerationSessionIdRef.current = sessionId;
@@ -2341,6 +2357,9 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
if (!sessionId) {
return;
}
+ const priorQuestion = workspaceQuestion;
+ const summary = runningSummaryRef.current;
+ const history = conversationHistoryRef.current;
try {
await stopPlanningGeneration(sessionId, projectId);
@@ -2354,13 +2373,31 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
setIsAutoRetrying(false);
setIsRefiningSummary(false);
refineSummaryInFlightRef.current = false;
- setView({
- type: "error",
- session: { sessionId, currentQuestion: null, summary: null },
- errorMessage: t("planning.generationStopped", "Generation stopped by user. You can retry or start a new session."),
- });
+ setError(null);
setStreamingOutput("");
- }, [projectId, t]);
+ setGenerationStartTime(null);
+
+ if (priorQuestion) {
+ const answered = history.some((entry) => entry.question?.id === priorQuestion.id && entry.response);
+ setEditingQuestionId(answered ? priorQuestion.id : null);
+ setWorkspaceQuestion(priorQuestion);
+ setView({
+ type: "question",
+ session: { sessionId, currentQuestion: priorQuestion, summary },
+ });
+ } else if (summary) {
+ setWorkspaceQuestion(null);
+ setView({
+ type: "plan_review",
+ session: { sessionId, currentQuestion: null, summary },
+ summary,
+ });
+ } else {
+ draftSessionIdRef.current = sessionId;
+ setInitialPlan(_activePlanPrompt);
+ setView({ type: "initial" });
+ }
+ }, [_activePlanPrompt, projectId, workspaceQuestion]);
const handleRetryFromError = useCallback(async () => {
if (view.type !== "error") {
@@ -2383,6 +2420,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
setError(null);
setGenerationActivity("question");
setIsRefineMenuOpen(false);
+ setGenerationStartTime(Date.now());
setView({ type: "loading" });
try {
const response = await respondToPlanning(sessionId, { refine: true, focus: refinementInstructions }, projectId);
diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx
index a9fda7b8c2..0e82c44087 100644
--- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx
+++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { PlanningModeModal } from "../PlanningModeModal";
-import { mockCreatePlanningDraft, mockFetchAiSession, mockFetchAiSessions, mockRespondToPlanning, mockStartPlanningStreaming, mockValidatePlanningSession, mockCreateTaskFromPlanning, mockTasks, mockSummary } from "./PlanningModeModal.test-helpers";
+import { mockCreatePlanningDraft, mockFetchAiSession, mockFetchAiSessions, mockRespondToPlanning, mockStartPlanningStreaming, mockStopPlanningGeneration, mockValidatePlanningSession, mockCreateTaskFromPlanning, mockTasks, mockSummary } from "./PlanningModeModal.test-helpers";
const mockViewportMode = vi.hoisted(() => vi.fn(() => "desktop" as "desktop" | "mobile"));
const mockConnectPlanningStream = vi.hoisted(() => vi.fn());
@@ -24,7 +24,7 @@ vi.mock("../../api", () => {
fetchAiSession: (...args: unknown[]) => mockFetchAiSession(...args), fetchAiSessions: (...args: unknown[]) => mockFetchAiSessions(...args),
respondToPlanning: (...args: unknown[]) => mockRespondToPlanning(...args), validatePlanningSession: (...args: unknown[]) => mockValidatePlanningSession(...args), createTaskFromPlanning: (...args: unknown[]) => mockCreateTaskFromPlanning(...args),
fetchSettings: fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }), fetchGlobalSettings: fn().mockResolvedValue({}), fetchModels: fn().mockResolvedValue([]), fetchWorkflowSteps: fn().mockResolvedValue([]), fetchBoardWorkflows: fn().mockResolvedValue({ workflows: [] }),
- startPlanning: fn(), startPlanningStreaming: (...args: unknown[]) => mockStartPlanningStreaming(...args), createPlanningDraft: (...args: unknown[]) => mockCreatePlanningDraft(...args), connectPlanningStream: (...args: unknown[]) => mockConnectPlanningStream(...args), rewindPlanningSession: fn(), retryPlanningSession: fn(), cancelPlanning: fn(), stopPlanningGeneration: fn(), updatePlanningSessionDraft: fn(), updatePlanningSessionTitle: fn(), startPlanningBreakdown: fn(), createTasksFromPlanning: fn(), parseConversationHistory: (raw: string) => JSON.parse(raw || "[]"), acquireSessionLock: fn(), releaseSessionLock: fn(), forceAcquireSessionLock: fn(), uploadAttachment: fn(), deleteAttachment: fn(), updateTask: fn(), pauseTask: fn(), unpauseTask: fn(), fetchTaskDetail: fn(), requestSpecRevision: fn(), approvePlan: fn(), rejectPlan: fn(), refineTask: fn(), deleteAiSession: fn(), refineText: fn(), getRefineErrorMessage: (error: Error) => error.message,
+ startPlanning: fn(), startPlanningStreaming: (...args: unknown[]) => mockStartPlanningStreaming(...args), createPlanningDraft: (...args: unknown[]) => mockCreatePlanningDraft(...args), connectPlanningStream: (...args: unknown[]) => mockConnectPlanningStream(...args), rewindPlanningSession: fn(), retryPlanningSession: fn(), cancelPlanning: fn(), stopPlanningGeneration: (...args: unknown[]) => mockStopPlanningGeneration(...args), updatePlanningSessionDraft: fn(), updatePlanningSessionTitle: fn(), startPlanningBreakdown: fn(), createTasksFromPlanning: fn(), parseConversationHistory: (raw: string) => JSON.parse(raw || "[]"), acquireSessionLock: fn(), releaseSessionLock: fn(), forceAcquireSessionLock: fn(), uploadAttachment: fn(), deleteAttachment: fn(), updateTask: fn(), pauseTask: fn(), unpauseTask: fn(), fetchTaskDetail: fn(), requestSpecRevision: fn(), approvePlan: fn(), rejectPlan: fn(), refineTask: fn(), deleteAiSession: fn(), refineText: fn(), getRefineErrorMessage: (error: Error) => error.message,
};
});
@@ -39,7 +39,7 @@ const summaryWithRefinements = {
};
describe("PlanningModeModal sequential flow", () => {
- beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockPlanningSse.events = null; mockViewportMode.mockReturnValue("desktop"); mockFetchAiSessions.mockResolvedValue([]); mockCreatePlanningDraft.mockResolvedValue({ sessionId: "draft-1", title: "Secure plan" }); mockStartPlanningStreaming.mockResolvedValue({ sessionId: "draft-1" }); mockValidatePlanningSession.mockResolvedValue({ summary: mockSummary, validated: true }); mockCreateTaskFromPlanning.mockResolvedValue({ id: "FN-8442" }); });
+ beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); mockPlanningSse.events = null; mockViewportMode.mockReturnValue("desktop"); mockFetchAiSessions.mockResolvedValue([]); mockCreatePlanningDraft.mockResolvedValue({ sessionId: "draft-1", title: "Secure plan" }); mockStartPlanningStreaming.mockResolvedValue({ sessionId: "draft-1" }); mockStopPlanningGeneration.mockResolvedValue({ success: true }); mockValidatePlanningSession.mockResolvedValue({ summary: mockSummary, validated: true }); mockCreateTaskFromPlanning.mockResolvedValue({ id: "FN-8442" }); });
it("persists a draft before generation and immediately shows initial-plan progress", async () => {
render();
fireEvent.change(screen.getByLabelText("What do you want to build?"), { target: { value: "Build secure accounts" } });
@@ -357,6 +357,49 @@ describe("PlanningModeModal sequential flow", () => {
await waitFor(() => expect(mockConnectPlanningStream).toHaveBeenCalledTimes(1));
expect(mockConnectPlanningStream).toHaveBeenCalledWith("session-1", "project-1", expect.any(Object));
});
+ it("keeps elapsed thinking time scoped to each generating session", async () => {
+ const now = Date.parse("2026-07-21T08:00:30.000Z");
+ const dateNow = vi.spyOn(Date, "now").mockReturnValue(now);
+ mockFetchAiSession.mockImplementation(async (sessionId: string) => ({
+ ...base,
+ id: sessionId,
+ status: "generating",
+ currentQuestion: null,
+ result: JSON.stringify(summaryWithRefinements),
+ inputPayload: JSON.stringify({
+ generationPurpose: "plan_update",
+ generationStartedAt: new Date(now - (sessionId === "session-1" ? 25_000 : 7_000)).toISOString(),
+ }),
+ }));
+ const props = { isOpen: true, onClose: vi.fn(), onTaskCreated: vi.fn(), onTasksCreated: vi.fn(), tasks: mockTasks, projectId: "project-1" };
+ const { rerender } = render();
+
+ expect(await screen.findByText("Thinking… (25s)")).toBeInTheDocument();
+
+ rerender();
+ expect(await screen.findByText("Thinking… (7s)")).toBeInTheDocument();
+ dateNow.mockRestore();
+ });
+ it("returns to the prior question without an error when generation is stopped", async () => {
+ const priorQuestion = { id: "q-prior", type: "text", question: "What should change?" };
+ mockFetchAiSession.mockResolvedValue({
+ ...base,
+ status: "generating",
+ currentQuestion: null,
+ result: JSON.stringify(summaryWithRefinements),
+ conversationHistory: JSON.stringify([{ question: priorQuestion, response: { "q-prior": "Preserve drafts" } }]),
+ inputPayload: JSON.stringify({ generationPurpose: "plan_update", generationStartedAt: new Date().toISOString() }),
+ });
+ renderSession({});
+
+ await waitFor(() => expect(mockConnectPlanningStream).toHaveBeenCalledWith("session-1", "project-1", expect.any(Object)));
+ fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
+
+ await waitFor(() => expect(mockStopPlanningGeneration).toHaveBeenCalledWith("session-1", "project-1"));
+ expect(await screen.findByText("What should change?")).toBeInTheDocument();
+ expect(screen.getByPlaceholderText("Type your answer here...")).toHaveValue("Preserve drafts");
+ expect(screen.queryByText(/Generation stopped by user/i)).toBeNull();
+ });
it("renders exactly one write-your-own choice for normalized select questions", async () => {
mockFetchAiSession.mockResolvedValue({
...base,
diff --git a/packages/dashboard/src/__tests__/planning-generation-cancellation.test.ts b/packages/dashboard/src/__tests__/planning-generation-cancellation.test.ts
index 8a6ff1547a..88bf313201 100644
--- a/packages/dashboard/src/__tests__/planning-generation-cancellation.test.ts
+++ b/packages/dashboard/src/__tests__/planning-generation-cancellation.test.ts
@@ -28,6 +28,7 @@ import {
const MOCK_TASK_STORE = {
listTasks: vi.fn(async () => []),
+ getSettings: vi.fn(async () => ({})),
getTask: vi.fn(async () => {
throw new Error("not found");
}),
@@ -74,7 +75,7 @@ describe("planning generation cancellation", () => {
expect(promptSignal?.aborted).toBe(true);
expect(dispose).toHaveBeenCalledTimes(1);
- expect((await getSession(sessionId))?.error).toMatch(/stopped by user/i);
+ expect((await getSession(sessionId))?.error).toBeUndefined();
resolveHungPrompt?.();
await new Promise((resolve) => setTimeout(resolve, 0));
diff --git a/packages/dashboard/src/planning.ts b/packages/dashboard/src/planning.ts
index 18cebd603e..e498672902 100644
--- a/packages/dashboard/src/planning.ts
+++ b/packages/dashboard/src/planning.ts
@@ -304,6 +304,8 @@ export const DRAFT_PLACEHOLDER_TITLE = "New planning session";
export interface DraftInputPayload {
initialPlan?: string;
generationPurpose?: "initial_plan" | "plan_update" | "question";
+ generationStartedAt?: string;
+ generationReturnQuestion?: PlanningQuestion;
clarificationEnabled?: boolean;
lastMailboxNotifiedQuestionKey?: string;
modelProvider?: string;
@@ -341,7 +343,6 @@ export const GENERATION_LOOP_REPEAT_LIMIT = 8;
const PLANNING_STUCK_ERROR_MESSAGE = "AI generation appears stuck with no new output. You can retry or start a new session.";
const PLANNING_LOOP_ERROR_MESSAGE = "AI generation appears stuck repeating the same output. You can retry or start a new session.";
-const PLANNING_USER_STOP_ERROR_MESSAGE = "Generation stopped by user. You can retry or start a new session.";
// ── Types ───────────────────────────────────────────────────────────────────
@@ -401,6 +402,10 @@ interface Session {
claimStartedAt?: string;
/** Whether the current generation must end at plan review rather than a question. */
generationPurpose?: "initial_plan" | "plan_update" | "question";
+ /** Durable start time for the active turn so each concurrent session owns its elapsed clock. */
+ generationStartedAt?: string;
+ /** Question restored when the user stops the active turn. */
+ generationReturnQuestion?: PlanningQuestion;
/** Last terminal error for retry UX */
error?: string;
/** AI agent session for real-time interaction */
@@ -612,6 +617,8 @@ function persistSession(session: Session, status: "generating" | "awaiting_input
? { clarificationEnabled: session.clarificationEnabled }
: {}),
...(session.generationPurpose ? { generationPurpose: session.generationPurpose } : {}),
+ ...(session.generationStartedAt ? { generationStartedAt: session.generationStartedAt } : {}),
+ ...(session.generationReturnQuestion ? { generationReturnQuestion: session.generationReturnQuestion } : {}),
...(session.lastMailboxNotifiedQuestionKey ? { lastMailboxNotifiedQuestionKey: session.lastMailboxNotifiedQuestionKey } : {}),
}),
conversationHistory: JSON.stringify(session.history),
@@ -676,6 +683,21 @@ function unpersistSession(sessionId: string): Promise {
return queued;
}
+/*
+FNXC:PlanningMode 2026-07-21-00:25:
+Each active planning turn owns a durable clock and return point. A modal-level Date.now()
+clock makes concurrent sessions appear synchronized, while clearing the question before
+generation leaves Stop with nowhere safe to return. Persist both at the turn boundary.
+*/
+function beginPlanningGeneration(
+ session: Session,
+ purpose: NonNullable,
+): void {
+ session.generationPurpose = purpose;
+ session.generationStartedAt = new Date().toISOString();
+ session.generationReturnQuestion = session.currentQuestion ?? session.generationReturnQuestion;
+}
+
/** Release in-memory planning runtime state while keeping persisted history. */
export function releaseSession(sessionId: string): void {
cleanupInMemorySession(sessionId);
@@ -752,6 +774,12 @@ function buildSessionFromRow(row: AiSessionRow): Session {
|| payload.generationPurpose === "question"
? payload.generationPurpose
: undefined,
+ generationStartedAt: typeof payload.generationStartedAt === "string"
+ ? payload.generationStartedAt
+ : undefined,
+ generationReturnQuestion: payload.generationReturnQuestion && typeof payload.generationReturnQuestion === "object"
+ ? normalizePlanningQuestion(payload.generationReturnQuestion, payload.initialPlan ?? row.title)
+ : undefined,
lastMailboxNotifiedQuestionKey: typeof payload.lastMailboxNotifiedQuestionKey === "string"
? payload.lastMailboxNotifiedQuestionKey
: undefined,
@@ -1083,6 +1111,7 @@ export async function createSession(
};
sessions.set(sessionId, session);
+ beginPlanningGeneration(session, "initial_plan");
persistSession(session, "generating");
const systemPrompt = await resolvePlanningModeSystemPrompt(store, promptOverrides, session.workflowId);
@@ -1588,7 +1617,7 @@ export async function startExistingSession(
session.ntfyConfig = runtimeOptions.ntfyConfig;
session.messageStore = runtimeOptions.messageStore;
}
- session.generationPurpose = "initial_plan";
+ beginPlanningGeneration(session, "initial_plan");
await persistSession(session, "generating");
planningStreamManager.registerInitialTurn(sessionId, () => {
session.pluginRunner = pluginRunner;
@@ -1672,7 +1701,7 @@ export async function createSessionWithAgent(
};
sessions.set(sessionId, session);
- session.generationPurpose = "initial_plan";
+ beginPlanningGeneration(session, "initial_plan");
await persistSession(session, "generating");
planningStreamManager.registerInitialTurn(sessionId, () => {
@@ -2104,14 +2133,6 @@ async function runGenerationWithTimeout(session: Session, operation: (abortSi
try {
return await Promise.race([operation(abortController.signal), abortPromise]);
- } catch (error) {
- if (error instanceof Error && error.name === "AbortError") {
- const reason = generationRecord.abortReason;
- if (reason === "user-stop" && !session.error) {
- setSessionError(session, PLANNING_USER_STOP_ERROR_MESSAGE);
- }
- }
- throw error;
} finally {
clearTimeout(generationRecord.timer);
if (activeGenerations.get(session.id) === generationRecord) {
@@ -2480,6 +2501,8 @@ async function continueAgentConversation(session: Session, message: string): Pro
session.lastGeneratedThinking = session.thinkingOutput;
session.updatedAt = new Date();
session.generationPurpose = undefined;
+ session.generationStartedAt = undefined;
+ session.generationReturnQuestion = undefined;
session.currentQuestion = coerceQuestionResponse(parsed, session);
await persistSession(session, "awaiting_input");
planningStreamManager.broadcast(session.id, { type: "summary", data: session.summary });
@@ -2835,8 +2858,8 @@ export async function submitResponse(
if (isRefineRequest(responses) && session.summary) {
// Refinement steers which question comes next; it is never an answer to the
// currently displayed question and therefore must not create a history entry.
+ beginPlanningGeneration(session, "question");
session.currentQuestion = undefined;
- session.generationPurpose = "question";
session.error = undefined;
await persistSession(session, "generating");
@@ -2877,8 +2900,8 @@ export async function submitResponse(
// Clear the answered question while generation is active so reconnects cannot replay it.
// The completed turn persists and broadcasts exactly one newly generated question.
+ beginPlanningGeneration(session, "plan_update");
session.currentQuestion = undefined;
- session.generationPurpose = "plan_update";
await persistSession(session, "generating");
if (!session.agent) {
// An edited older answer must be replayed in its original position with every
@@ -2962,7 +2985,7 @@ export async function retrySession(
*/
session.currentQuestion = undefined;
session.updatedAt = new Date();
- session.generationPurpose = session.history.length === 0 ? "initial_plan" : "plan_update";
+ beginPlanningGeneration(session, session.history.length === 0 ? "initial_plan" : "plan_update");
await persistSession(session, "generating");
if (session.history.length === 0) {
@@ -3058,7 +3081,32 @@ export function stopGeneration(sessionId: string): boolean {
activeGeneration.abortController.abort();
activeGenerations.delete(sessionId);
- setSessionError(session, PLANNING_USER_STOP_ERROR_MESSAGE);
+ const returnQuestion = session.generationReturnQuestion;
+ const stoppedPurpose = session.generationPurpose;
+ session.error = undefined;
+ session.thinkingOutput = "";
+ session.generationPurpose = undefined;
+ session.generationStartedAt = undefined;
+ session.generationReturnQuestion = undefined;
+ session.updatedAt = new Date();
+
+ if (returnQuestion) {
+ session.currentQuestion = returnQuestion;
+ session.editingQuestionId = session.history.some((entry) => entry.question.id === returnQuestion.id)
+ ? returnQuestion.id
+ : undefined;
+ session.summary = buildRunningSummary(session.initialPlan, session.history, session.summary);
+ void persistSession(session, "awaiting_input");
+ planningStreamManager.broadcast(session.id, { type: "summary", data: session.summary });
+ planningStreamManager.broadcast(session.id, { type: "question", data: returnQuestion });
+ } else {
+ session.currentQuestion = undefined;
+ session.editingQuestionId = undefined;
+ void persistSession(session, stoppedPurpose === "initial_plan" ? "draft" : "awaiting_input");
+ if (session.summary && stoppedPurpose !== "initial_plan") {
+ planningStreamManager.broadcast(session.id, { type: "summary", data: session.summary });
+ }
+ }
return true;
}