FN-8437: restore Planning Mode sessions after navigation

Persist active Planning interviews across embedded navigation while honoring explicit dismissal.

- Store the active Planning session per project and restore it through the existing session loader.
- Clear the saved active session for dismissals and terminal session actions.
- Cover leave-and-return, completed-away, fresh-plan, and dismissed-error flows; document the behavior.
- Add a patch changeset for the Planning session recovery fix.

Files changed:
 .changeset/fn-8437-planning-leave-return-restore.md       |  7 ++
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/components/PlanningModeModal.tsx | 51 ++++++++++++-
 packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx | 86 ++++++++++++++++++++++
 packages/dashboard/app/hooks/__tests__/modalPersistence.test.ts | 18 +++++
 packages/dashboard/app/hooks/modalPersistence.ts   | 17 +++++
 packages/dashboard/app/utils/__tests__/projectStorage.test.ts |  3 +-
 packages/dashboard/app/utils/projectStorage.ts     |  1 +
 8 files changed, 180 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-8437

Fusion-Task-Lineage: 892aacb6-139e-4487-a40c-1bedd51237ea

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-20 12:09:23 -07:00
parent baf88931d4
commit 8c01cc7c7a
8 changed files with 180 additions and 5 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Restore in-progress Planning Mode interviews after leave/return, including mid-generation.
category: fix
dev: Persists last-active planning session id project-scoped; remount rehydrates via loadSession without auto-retry.

View File

@@ -523,7 +523,7 @@ Planning is a desktop/tablet left-sidebar main-content destination after **Comma
When a Planning session needs your input or needs attention, open the docked Planning view from the **Planning** navigation item. Its yellow needs-input dot is visible on the desktop left sidebar and mobile More controls. Non-planning in-progress, needs-input, and error sessions appear in the session notification banner, where available Resume actions reconnect to their matching surface. When a Planning session needs your input or needs attention, open the docked Planning view from the **Planning** navigation item. Its yellow needs-input dot is visible on the desktop left sidebar and mobile More controls. Non-planning in-progress, needs-input, and error sessions appear in the session notification banner, where available Resume actions reconnect to their matching surface.
<!-- FNXC:PlanningRetry 2026-07-15-00:00: FN-8332 confines automatic Planning Mode retry to failures observed by an active in-session SSE/poll turn. Browser reload or session resume must restore the persisted progress/error verbatim and leave retry as an explicit user choice. --> <!-- FNXC:PlanningRetry 2026-07-15-00:00: FN-8332 confines automatic Planning Mode retry to failures observed by an active in-session SSE/poll turn. Browser reload or session resume must restore the persisted progress/error verbatim and leave retry as an explicit user choice. -->
When an active Planning AI generation appears stuck, Planning Mode automatically retries the same session up to three times and shows **Retrying… (attempt N of 3)** before falling back to the permanent **Retry**/**Dismiss** error panel. Any successful question or summary progress resets the automatic retry budget. Reopening or reloading a saved Planning session restores its saved question, summary, thinking, or error without starting another generation; choose **Retry** explicitly from a restored error panel if you want to run it again. When an active Planning AI generation appears stuck, Planning Mode automatically retries the same session up to three times and shows **Retrying… (attempt N of 3)** before falling back to the permanent **Retry**/**Dismiss** error panel. Any successful question or summary progress resets the automatic retry budget. Leaving Planning—including while a next question is generating—and returning restores the last active interview for that project: a generation reconnects with its saved thinking, while a turn that completed while away shows its new question and running plan. Reopening or reloading a saved Planning session restores its saved question, summary, thinking, or error without starting another generation; choose **Retry** explicitly from a restored error panel if you want to run it again.
<!-- FNXC:PlanningMode 2026-07-19-15:55: FN-8400 replaces the duplicate prompt-recovery controls with a focused three-pane interview; restarting remains a deliberate New session action. --> <!-- FNXC:PlanningMode 2026-07-19-15:55: FN-8400 replaces the duplicate prompt-recovery controls with a focused three-pane interview; restarting remains a deliberate New session action. -->
Use **New session** to restart planning with a different idea. Use **New session** to restart planning with a different idea.

View File

@@ -49,6 +49,9 @@ import {
savePlanningDescription, savePlanningDescription,
getPlanningDescription, getPlanningDescription,
clearPlanningDescription, clearPlanningDescription,
savePlanningActiveSession,
getPlanningActiveSession,
clearPlanningActiveSession,
} from "../hooks/modalPersistence"; } from "../hooks/modalPersistence";
import { getRelativeTimeBucket } from "../utils/relativeTimeAgo"; import { getRelativeTimeBucket } from "../utils/relativeTimeAgo";
import { Lightbulb, X, Loader2, CheckCircle, ArrowLeft, ArrowRight, Sparkles, ListTree, GripVertical, ArrowUp, ArrowDown, Plus, Trash2, RefreshCw, ChevronLeft, MessageSquarePlus, AlertCircle, Clock, HelpCircle, StopCircle, Archive, ArchiveRestore, Pencil } from "lucide-react"; import { Lightbulb, X, Loader2, CheckCircle, ArrowLeft, ArrowRight, Sparkles, ListTree, GripVertical, ArrowUp, ArrowDown, Plus, Trash2, RefreshCw, ChevronLeft, MessageSquarePlus, AlertCircle, Clock, HelpCircle, StopCircle, Archive, ArchiveRestore, Pencil } from "lucide-react";
@@ -425,6 +428,10 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
// identity change (e.g. typing into the textarea recreates loadSession) and // identity change (e.g. typing into the textarea recreates loadSession) and
// yanks the user back into the previous session's question view. // yanks the user back into the previous session's question view.
const dismissedResumeRef = useRef<string | null>(null); const dismissedResumeRef = useRef<string | null>(null);
// A mount only needs one storage-backed resume decision. Re-reading after a
// user intentionally starts fresh would otherwise pull the old interview
// back in when unrelated callbacks change identity.
const hasAttemptedStoredResumeRef = useRef(false);
useEffect(() => { useEffect(() => {
viewRef.current = view; viewRef.current = view;
@@ -1320,6 +1327,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
// The session was deleted (commonly: this tab just turned it into // The session was deleted (commonly: this tab just turned it into
// tasks via Create Task / Create Tasks). Quietly fall back to the // tasks via Create Task / Create Tasks). Quietly fall back to the
// new-session view rather than surfacing a scary error banner. // new-session view rather than surfacing a scary error banner.
clearPlanningActiveSession(projectId);
setSelectedSessionId(null); setSelectedSessionId(null);
setMobileShowDetail(false); setMobileShowDetail(false);
setActivePlanPrompt(""); setActivePlanPrompt("");
@@ -1476,7 +1484,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
}); });
} }
}, },
[connectToPlanningStream, projectId, resetPlanningAutoRetryBudget], [connectToPlanningStream, projectId, resetPlanningAutoRetryBudget, t],
); );
// Resume the externally-requested session when the modal first opens. // Resume the externally-requested session when the modal first opens.
@@ -1494,6 +1502,28 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
void loadSession(resumeSessionId); void loadSession(resumeSessionId);
}, [isOpen, resumeSessionId]); }, [isOpen, resumeSessionId]);
// Restore the persisted active interview for ordinary Planning navigation.
// Explicit resume props and seeded opens own their destination and must not
// be replaced by a prior session.
useEffect(() => {
if (!isOpen || resumeSessionId || initialPlanProp || selectedSessionId || hasAttemptedStoredResumeRef.current) return;
hasAttemptedStoredResumeRef.current = true;
const storedSessionId = getPlanningActiveSession(projectId);
if (!storedSessionId || dismissedResumeRef.current === storedSessionId) return;
setSelectedSessionId(storedSessionId);
setMobileShowDetail(true);
void loadSession(storedSessionId);
}, [initialPlanProp, isOpen, projectId, resumeSessionId, selectedSessionId]);
// Keep the focused interview durable before embedded Planning unmounts on a
// main-content navigation change. Selection writes cover starts, sidebar
// picks, explicit resumes, and storage-backed restores with one authority.
useEffect(() => {
if (selectedSessionId) {
savePlanningActiveSession(selectedSessionId, projectId);
}
}, [projectId, selectedSessionId]);
// Re-sync the selected session whenever the planning screen is shown. // Re-sync the selected session whenever the planning screen is shown.
// loadSession tears down any existing stream and reconnects, so the right // loadSession tears down any existing stream and reconnects, so the right
// view always reflects the freshest server state for whatever row is // view always reflects the freshest server state for whatever row is
@@ -1622,13 +1652,14 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
if (resumeSessionId) { if (resumeSessionId) {
dismissedResumeRef.current = resumeSessionId; dismissedResumeRef.current = resumeSessionId;
} }
clearPlanningActiveSession(projectId);
const preserveActiveDraft = selectedSessionId === null && viewRef.current.type === "initial"; const preserveActiveDraft = selectedSessionId === null && viewRef.current.type === "initial";
resetDetailState({ preserveInitialPlan: preserveActiveDraft }); resetDetailState({ preserveInitialPlan: preserveActiveDraft });
setSelectedSessionId(null); setSelectedSessionId(null);
setShowSessionList(false); setShowSessionList(false);
setMobileShowDetail(true); setMobileShowDetail(true);
setNewSessionFocusSignal((signal) => signal + 1); setNewSessionFocusSignal((signal) => signal + 1);
}, [resetDetailState, resumeSessionId, selectedSessionId]); }, [projectId, resetDetailState, resumeSessionId, selectedSessionId]);
const handleBackToList = useCallback(() => { const handleBackToList = useCallback(() => {
setShowSessionList(true); setShowSessionList(true);
@@ -1782,6 +1813,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
streamConnectionRef.current?.close(); streamConnectionRef.current?.close();
streamConnectionRef.current = null; streamConnectionRef.current = null;
resetDetailState(); resetDetailState();
clearPlanningActiveSession(projectId);
setSelectedSessionId(null); setSelectedSessionId(null);
setMobileShowDetail(false); setMobileShowDetail(false);
} }
@@ -1820,6 +1852,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
streamConnectionRef.current?.close(); streamConnectionRef.current?.close();
streamConnectionRef.current = null; streamConnectionRef.current = null;
resetDetailState(); resetDetailState();
clearPlanningActiveSession(projectId);
setSelectedSessionId(null); setSelectedSessionId(null);
setMobileShowDetail(false); setMobileShowDetail(false);
} }
@@ -2142,6 +2175,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
// Single-task creation should preserve completed planning history, so // Single-task creation should preserve completed planning history, so
// only clear the active selection before closing; keep the sidebar row // only clear the active selection before closing; keep the sidebar row
// in local state to match persisted server truth. // in local state to match persisted server truth.
clearPlanningActiveSession(projectId);
setSelectedSessionId(null); setSelectedSessionId(null);
handleClose(); handleClose();
} catch (err) { } catch (err) {
@@ -2221,6 +2255,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
setPlanningModelId(undefined); setPlanningModelId(undefined);
setPlanningThinkingLevel(""); setPlanningThinkingLevel("");
currentSessionIdRef.current = null; currentSessionIdRef.current = null;
clearPlanningActiveSession(projectId);
setSelectedSessionId(null); setSelectedSessionId(null);
handleClose(); handleClose();
} catch (err) { } catch (err) {
@@ -2684,7 +2719,17 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
{isRetrying ? <Loader2 size={14} className="spin" /> : <RefreshCw size={14} />} {isRetrying ? <Loader2 size={14} className="spin" /> : <RefreshCw size={14} />}
<span className="icon-ml-6">{isRetrying ? t("planning.retrying", "Retrying...") : t("common.retry", "Retry")}</span> <span className="icon-ml-6">{isRetrying ? t("planning.retrying", "Retrying...") : t("common.retry", "Retry")}</span>
</button> </button>
<button className="btn" onClick={handleClose} disabled={isRetrying}>{t("planning.dismiss", "Dismiss")}</button> <button
className="btn"
onClick={() => {
// FNXC:PlanningMode 2026-07-20-12:15: FN-8437 treats error dismissal as an intentional exit from the resumable interview, so reopening Planning starts fresh rather than restoring the dismissed failure.
clearPlanningActiveSession(projectId);
handleClose();
}}
disabled={isRetrying}
>
{t("planning.dismiss", "Dismiss")}
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -191,6 +191,7 @@ describe("PlanningModeModal", () => {
MockEventSource.reset(); MockEventSource.reset();
vi.stubGlobal("EventSource", MockEventSource as any); vi.stubGlobal("EventSource", MockEventSource as any);
window.sessionStorage.clear(); window.sessionStorage.clear();
localStorage.clear();
// Default to desktop viewport; mobile-specific tests override per-test. // Default to desktop viewport; mobile-specific tests override per-test.
mockViewport("desktop"); mockViewport("desktop");
@@ -273,6 +274,91 @@ describe("PlanningModeModal", () => {
}); });
}); });
describe("last-active session restoration", () => {
const activeSession = (status: "generating" | "awaiting_input") => ({
id: "session-leave-return",
type: "planning" as const,
status,
title: "Restored planning interview",
inputPayload: JSON.stringify({ initialPlan: "Restore a planning interview" }),
conversationHistory: JSON.stringify([{ question: mockQuestion, response: { [mockQuestion.id]: "Small" } }]),
currentQuestion: status === "awaiting_input"
? JSON.stringify({ ...mockQuestion, id: "q-return", question: "What should happen after return?" })
: null,
result: JSON.stringify({ ...mockSummary, title: "Running plan restored" }),
thinkingOutput: "Persisted mid-generation reasoning",
error: null,
projectId: "project-restore",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
it.each(["desktop", "mobile"] as const)("FN-8437 restores a generating session after embedded leave/return on %s", async (viewportMode) => {
mockViewport(viewportMode);
mockFetchAiSession.mockResolvedValue(activeSession("generating"));
const firstMount = render(
<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} projectId="project-restore" resumeSessionId="session-leave-return" presentation="embedded" />,
);
await waitFor(() => expect(mockConnectPlanningStream).toHaveBeenCalledWith("session-leave-return", "project-restore", expect.anything()));
firstMount.unmount();
render(
<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} projectId="project-restore" presentation="embedded" />,
);
await waitFor(() => expect(mockFetchAiSession).toHaveBeenCalledTimes(2));
expect(await screen.findByText("AI is thinking...")).toBeInTheDocument();
expect(screen.getByText("Persisted mid-generation reasoning")).toBeInTheDocument();
expect(mockConnectPlanningStream).toHaveBeenLastCalledWith("session-leave-return", "project-restore", expect.anything());
expect(screen.queryByPlaceholderText(/e.g., Build a user authentication/)).toBeNull();
});
it("FN-8437 rehydrates the server question and running plan when generation finishes while away", async () => {
localStorage.setItem("kb:project-restore:kb-planning-active-session", "session-leave-return");
mockFetchAiSession.mockResolvedValue(activeSession("awaiting_input"));
render(
<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} projectId="project-restore" presentation="embedded" />,
);
expect(await screen.findByText("What should happen after return?")).toBeInTheDocument();
expect(screen.getByText("Running plan restored")).toBeInTheDocument();
expect(screen.getByText("What is the scope?")).toBeInTheDocument();
expect(screen.queryByPlaceholderText(/e.g., Build a user authentication/)).toBeNull();
});
it("FN-8437 clears the active session when dismissing a restored error", async () => {
localStorage.setItem("kb:project-restore:kb-planning-active-session", "session-leave-return");
mockFetchAiSession.mockResolvedValue({
...activeSession("awaiting_input"),
status: "error",
currentQuestion: null,
error: "Restored planning failure",
});
render(
<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} projectId="project-restore" presentation="embedded" />,
);
expect(await screen.findByRole("alert")).toHaveTextContent("Restored planning failure");
fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
expect(mockOnClose).toHaveBeenCalledTimes(1);
expect(localStorage.getItem("kb:project-restore:kb-planning-active-session")).toBeNull();
});
it("FN-8437 keeps an initial-plan handoff fresh instead of restoring the last active session", async () => {
localStorage.setItem("kb:project-restore:kb-planning-active-session", "session-leave-return");
render(
<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} projectId="project-restore" initialPlan="Intentional new plan" presentation="embedded" />,
);
await waitFor(() => expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Intentional new plan", "project-restore", undefined, { clarificationEnabled: true }, undefined));
expect(mockFetchAiSession).not.toHaveBeenCalled();
});
});
describe("Planning flow", () => { describe("Planning flow", () => {
it.each(["desktop", "mobile"] as const)("FN-6977 keeps malformed live running plans non-terminal on %s", async (viewportMode) => { it.each(["desktop", "mobile"] as const)("FN-6977 keeps malformed live running plans non-terminal on %s", async (viewportMode) => {
mockViewport(viewportMode); mockViewport(viewportMode);

View File

@@ -1,11 +1,15 @@
import { describe, it, expect, beforeEach, vi } from "vitest"; import { describe, it, expect, beforeEach, vi } from "vitest";
import { import {
STORED_PLANNING_KEY, STORED_PLANNING_KEY,
STORED_PLANNING_ACTIVE_SESSION_KEY,
STORED_SUBTASK_KEY, STORED_SUBTASK_KEY,
STORED_MISSION_KEY, STORED_MISSION_KEY,
savePlanningDescription, savePlanningDescription,
getPlanningDescription, getPlanningDescription,
clearPlanningDescription, clearPlanningDescription,
savePlanningActiveSession,
getPlanningActiveSession,
clearPlanningActiveSession,
saveSubtaskDescription, saveSubtaskDescription,
getSubtaskDescription, getSubtaskDescription,
clearSubtaskDescription, clearSubtaskDescription,
@@ -25,6 +29,10 @@ describe("modalPersistence", () => {
expect(STORED_PLANNING_KEY).toBe("kb-planning-last-description"); expect(STORED_PLANNING_KEY).toBe("kb-planning-last-description");
}); });
it("exports planning active-session key", () => {
expect(STORED_PLANNING_ACTIVE_SESSION_KEY).toBe("kb-planning-active-session");
});
it("exports subtask key", () => { it("exports subtask key", () => {
expect(STORED_SUBTASK_KEY).toBe("kb-subtask-last-description"); expect(STORED_SUBTASK_KEY).toBe("kb-subtask-last-description");
}); });
@@ -77,6 +85,16 @@ describe("modalPersistence", () => {
}); });
}); });
describe("Planning active-session persistence", () => {
it("saves, reads, and clears an active session per project", () => {
savePlanningActiveSession("planning-123", "proj-123");
expect(getPlanningActiveSession("proj-123")).toBe("planning-123");
expect(getPlanningActiveSession("proj-other")).toBe("");
clearPlanningActiveSession("proj-123");
expect(getPlanningActiveSession("proj-123")).toBe("");
});
});
describe("Subtask persistence", () => { describe("Subtask persistence", () => {
it("saves and retrieves subtask description", () => { it("saves and retrieves subtask description", () => {
saveSubtaskDescription("Implement login feature"); saveSubtaskDescription("Implement login feature");

View File

@@ -2,6 +2,7 @@ import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/project
// Storage keys — each modal type has independent storage // Storage keys — each modal type has independent storage
export const STORED_PLANNING_KEY = "kb-planning-last-description"; export const STORED_PLANNING_KEY = "kb-planning-last-description";
export const STORED_PLANNING_ACTIVE_SESSION_KEY = "kb-planning-active-session";
export const STORED_SUBTASK_KEY = "kb-subtask-last-description"; export const STORED_SUBTASK_KEY = "kb-subtask-last-description";
export const STORED_MISSION_KEY = "kb-mission-last-goal"; export const STORED_MISSION_KEY = "kb-mission-last-goal";
export const STORED_GITHUB_IMPORT_KEY = "kb-dashboard-github-import-state"; export const STORED_GITHUB_IMPORT_KEY = "kb-dashboard-github-import-state";
@@ -20,6 +21,22 @@ export function clearPlanningDescription(projectId?: string): void {
removeScopedItem(STORED_PLANNING_KEY, projectId); removeScopedItem(STORED_PLANNING_KEY, projectId);
} }
/*
FNXC:PlanningMode 2026-07-20-12:00:
Embedded Planning unmounts whenever main-content navigation leaves its view. FN-8437 keeps the last active interview id project-scoped, matching Chat's active-session persistence, so a return during generation can rehydrate through the modal's single loadSession path.
*/
export function savePlanningActiveSession(sessionId: string, projectId?: string): void {
setScopedItem(STORED_PLANNING_ACTIVE_SESSION_KEY, sessionId, projectId);
}
export function getPlanningActiveSession(projectId?: string): string {
return getScopedItem(STORED_PLANNING_ACTIVE_SESSION_KEY, projectId) || "";
}
export function clearPlanningActiveSession(projectId?: string): void {
removeScopedItem(STORED_PLANNING_ACTIVE_SESSION_KEY, projectId);
}
// Subtask persistence // Subtask persistence
export function saveSubtaskDescription(description: string, projectId?: string): void { export function saveSubtaskDescription(description: string, projectId?: string): void {

View File

@@ -93,6 +93,7 @@ describe("projectStorage", () => {
"fn-agent-view", "fn-agent-view",
"kb-terminal-tabs", "kb-terminal-tabs",
"kb-planning-last-description", "kb-planning-last-description",
"kb-planning-active-session",
"kb-subtask-last-description", "kb-subtask-last-description",
"kb-mission-last-goal", "kb-mission-last-goal",
"kb-usage-view-mode", "kb-usage-view-mode",
@@ -114,7 +115,7 @@ describe("projectStorage", () => {
FNXC:ProjectStorage 2026-07-14-19:20: FNXC:ProjectStorage 2026-07-14-19:20:
Keep PROJECT_STORAGE_KEYS length lockstep with the source array (todo hide-done, github import state, github setup warning dismissals). Keep PROJECT_STORAGE_KEYS length lockstep with the source array (todo hide-done, github import state, github setup warning dismissals).
*/ */
expect(PROJECT_STORAGE_KEYS).toHaveLength(32); expect(PROJECT_STORAGE_KEYS).toHaveLength(33);
}); });
it("stores branch filter values as scoped strings per project", () => { it("stores branch filter values as scoped strings per project", () => {

View File

@@ -26,6 +26,7 @@ export const PROJECT_STORAGE_KEYS: string[] = [
"fn-agent-view", "fn-agent-view",
"kb-terminal-tabs", "kb-terminal-tabs",
"kb-planning-last-description", "kb-planning-last-description",
"kb-planning-active-session",
"kb-subtask-last-description", "kb-subtask-last-description",
"kb-mission-last-goal", "kb-mission-last-goal",
"kb-usage-view-mode", "kb-usage-view-mode",