From 8bd395a0c7bfcba584dd003948ec07992fcf8be5 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 6 Apr 2026 16:53:48 +0000 Subject: [PATCH] fix: resolve task lane stale state, partial submit, and closure bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import and call clearTaskState before updating questions/actions in handleSend and handleTaskSubmit so new AI tasks always replace stale sessionStorage cache instead of being overridden by it - Include pending (not yet completed) tasks in the AI message on partial submit so the AI knows which tasks were left unanswered - Fix stale closure in TaskLane saveTaskLane useEffect — use refs for questions/actions so the debounced backend save always uses current values - Add responses field to pending_task_lane TypeScript type, removing the unsafe double-cast in selectChat - Instruct the AI to re-surface incomplete tasks unless ≥75% confident the information is no longer needed Co-Authored-By: Claude Sonnet 4.6 --- backend/app/services/assistant_chat_service.py | 5 +++++ frontend/src/components/assistant/TaskLane.tsx | 14 +++++++++++--- frontend/src/pages/AssistantChatPage.tsx | 11 ++++++++--- frontend/src/types/ai-session.ts | 2 +- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/app/services/assistant_chat_service.py b/backend/app/services/assistant_chat_service.py index 49c44ffe..6e17b8e3 100644 --- a/backend/app/services/assistant_chat_service.py +++ b/backend/app/services/assistant_chat_service.py @@ -77,6 +77,9 @@ scope narrows it to this endpoint. - JSON array of objects with `text` (required) and `context` (optional, 1 sentence) - 1-3 questions per response - Do NOT ask questions inline in your prose. ALL questions go in the marker. +- If the engineer's message contains tasks marked `_(not yet completed)_`, re-include \ +those as questions/actions in your next response UNLESS you are ≥75% confident the \ +information is no longer needed to resolve the issue. Default to keeping them. **[ACTIONS] marker format:** - JSON array of objects with `label` (required), `command` (optional), `description` (required) @@ -155,6 +158,8 @@ To create a fork, append this marker AFTER your [QUESTIONS]/[ACTIONS] markers: Every single response MUST contain [QUESTIONS] and/or [ACTIONS] markers with valid JSON. \ No exceptions. Not even when forking. A response without at least one of these markers \ will crash the UI. If you are unsure, include both. The markers are REQUIRED output, not optional. +If any tasks in the engineer's message are marked `_(not yet completed)_`, re-include them \ +in your markers unless you are ≥75% confident that information is no longer relevant. """ diff --git a/frontend/src/components/assistant/TaskLane.tsx b/frontend/src/components/assistant/TaskLane.tsx index 590f34b3..21bc91db 100644 --- a/frontend/src/components/assistant/TaskLane.tsx +++ b/frontend/src/components/assistant/TaskLane.tsx @@ -130,6 +130,14 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa } }, [handleMouseMove, handleMouseUp]) + // Refs so the debounced save always uses the latest questions/actions/tasks + const questionsRef = useRef(questions) + const actionsRef = useRef(actions) + const tasksRef = useRef(tasks) + useEffect(() => { questionsRef.current = questions }, [questions]) + useEffect(() => { actionsRef.current = actions }, [actions]) + useEffect(() => { tasksRef.current = tasks }, [tasks]) + // Save task state to sessionStorage on every change + debounce to backend const saveTimerRef = useRef | null>(null) useEffect(() => { @@ -139,9 +147,9 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa if (saveTimerRef.current) clearTimeout(saveTimerRef.current) saveTimerRef.current = setTimeout(() => { aiSessionsApi.saveTaskLane(sessionId, { - questions: questions.map(q => ({ text: q.text, context: q.context })), - actions: actions.map(a => ({ label: a.label, command: a.command, description: a.description })), - responses: tasks as unknown as Array>, + questions: questionsRef.current.map(q => ({ text: q.text, context: q.context })), + actions: actionsRef.current.map(a => ({ label: a.label, command: a.command, description: a.description })), + responses: tasksRef.current as unknown as Array>, }).catch(() => { /* silent — best-effort save */ }) }, 2000) return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) } diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index 9b6169cb..c1ad8308 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -12,7 +12,7 @@ import { analytics } from '@/lib/analytics' import { toast } from '@/lib/toast' import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar' import { ChatMessage } from '@/components/assistant/ChatMessage' -import { TaskLane } from '@/components/assistant/TaskLane' +import { TaskLane, clearTaskState } from '@/components/assistant/TaskLane' import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal' import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal' import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat' @@ -242,7 +242,7 @@ export default function AssistantChatPage() { if (q.length > 0 || a.length > 0) { // Pre-load user's saved responses into sessionStorage BEFORE setting props // so TaskLane can restore them on mount/prop-change - const responses = (detail.pending_task_lane as Record).responses as unknown[] | undefined + const responses = detail.pending_task_lane.responses if (responses && responses.length > 0) { try { sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses)) @@ -340,6 +340,7 @@ export default function AssistantChatPage() { const hasQuestions = response.questions && response.questions.length > 0 const hasActions = response.actions && response.actions.length > 0 if (hasQuestions || hasActions) { + if (activeChatId) clearTaskState(activeChatId) setActiveQuestions(response.questions || []) setActiveActions(response.actions || []) setShowTaskLane(true) @@ -358,7 +359,8 @@ export default function AssistantChatPage() { const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => { if (!activeChatId || loading) return - // Format task responses into a structured message for the AI + // Format task responses into a structured message for the AI. + // Pending tasks are included so the AI knows they weren't completed yet. const parts: string[] = [] for (const r of responses) { const name = r.type === 'question' ? `Q: ${r.text}` : r.label || 'Check' @@ -366,6 +368,8 @@ export default function AssistantChatPage() { parts.push(`**${name}:**\n\`\`\`\n${r.value.trim()}\n\`\`\``) } else if (r.state === 'skipped') { parts.push(`**${name}:** _(skipped)_`) + } else { + parts.push(`**${name}:** _(not yet completed)_`) } } const userMessage = parts.join('\n\n') @@ -385,6 +389,7 @@ export default function AssistantChatPage() { // Update task lane based on AI response const hasQuestions = response.questions && response.questions.length > 0 const hasActions = response.actions && response.actions.length > 0 + if (activeChatId) clearTaskState(activeChatId) if (hasQuestions || hasActions) { setActiveQuestions(response.questions || []) setActiveActions(response.actions || []) diff --git a/frontend/src/types/ai-session.ts b/frontend/src/types/ai-session.ts index dd2ae7b4..c8f90886 100644 --- a/frontend/src/types/ai-session.ts +++ b/frontend/src/types/ai-session.ts @@ -196,7 +196,7 @@ export interface AISessionDetail extends AISessionSummary { ticket_data: Record | null steps: AISessionStepResponse[] conversation_messages: Array<{ role: string; content: string }> - pending_task_lane: { questions: QuestionItem[]; actions: ActionItem[] } | null + pending_task_lane: { questions: QuestionItem[]; actions: ActionItem[]; responses?: Array> } | null is_branching: boolean active_branch_id: string | null }