feat(#689): persistent meal-planner session state to prevent RAG leakage#698
feat(#689): persistent meal-planner session state to prevent RAG leakage#698
Conversation
…leakage Add conversation-scoped, Room-backed meal-planner session store and wire it into the ChatViewModel prompt pipeline. During active meal-planner turns, the model receives structured session context (preferences, plan, current day) instead of relying on chat history — and episodic/RAG context is suppressed to prevent stale memory leakage. New files: - MealPlanSessionEntity: Room entity with conversationId PK, status, preferences, plan, and day tracking fields - MealPlanSessionDao: DAO with getByConversationId, upsert, delete methods - MealPlanSessionRepository: Convenience layer with createOrReset, updateStatus, updatePreferences, saveHighLevelPlan, advanceDay, markCompleted Changes to existing files: - KernelDatabase: add MealPlanSessionEntity, MIGRATION_23_24, version 24 - MemoryModule: add DAO/repository providers, include MIGRATION_23_24 - ChatViewModel: detect active meal-planner turns, suppress RAG context, inject structured meal-plan context block, force history replay Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…versation delete, fix imports - Cache session result to avoid double DB query in ChatViewModel - Use simple MealPlanSessionEntity type instead of fully qualified name - Clean up meal_plan_sessions when conversation is deleted - Rename effectiveRagContextForPrompt to effectiveRagContext (already shadowed) - Reorder VerboseLoggingPreferenceUseCase imports for consistency
Debug APK readyCommit: Updated on each push. Removed when PR is merged or closed. |
…ersation_id - Create MealPlannerCollectSkill, MealPlannerPlanSkill, MealPlannerRecipeSkill, MealPlannerCompleteSkill - Wire new skills into SkillsModule and KernelAIToolSet - Update LoadSkillSkill enum and error messages - Add conversation_id to buildMealPlanContext output - Update MealPlannerSkill instructions to reference conversation_id - Add unit tests for all new stage-specific skills Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…structions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Initial testing: You: Let's plan some meals To get started, could you tell me a bit about what you're looking for? For example:
To get the ball rolling, what kind of meals are you after? Are we talking quick weeknight dinners, or something a bit more involved weekend cooking? Since we're keeping it low lactose and focusing on beef and chicken for 4 people over 3 days, here is a quick plan: Day 1: Chicken Stir-fry with mixed vegetables and rice. How does that sound? We can go into more detail on any of those, or we can adjust the plan if you'd like. To make sure this is all good for you, I'll save this plan state now. Now, shall we dive into the recipes for Day 1? Or is there anything else you want to tweak first? Since you're low lactose, we'll make sure to use oil or plant-based sauces instead of anything dairy. Here's a simple recipe: Chicken Stir-fry with mixed vegetables and rice (Serves 4) Ingredients:
Method:
|
…689) - Refactor buildMealPlanContext to return only the instructions relevant to the current stage (collecting_preferences, high_level_plan_ready, generating_recipes, completed) instead of the full monolithic block - This reduces context noise and prevents the model from seeing all 4 stages at once, which caused the looping bug - Update MealPlannerRecipeSkill instructions with explicit step-by-step execution order and CRITICAL save call reminder - Update MealPlannerSkill to emphasize save_meal_plan_state is required to advance past the current day - Update BuildMealPlanContextTest to verify stage instructions are present Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…thic instructions (#689) The model was loading the full 170-line MealPlannerSkill instructions via load_skill, which contained all 4 stages and caused the infinite loop bug. The stage-specific instructions injected in buildMealPlanContext were fighting against the loaded skill instructions. Fix: MealPlannerSkill.fullInstructions now returns a short redirect that tells the model to load the appropriate stage skill based on session status: collecting_preferences → meal_planner_collect high_level_plan_ready → meal_planner_plan generating_recipes → meal_planner_recipe completed → tell user plan is done This ensures the model only ever has the relevant ~30-line instructions for its current stage, not all 170 lines at once. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Latest test results: You: Let's plan meals How many days are you looking to plan for, and what are your protein preferences (e.g., chicken, fish, vegetarian)? I've got the preferences down. What's the next step? Do you want me to start generating some meal ideas, or are there any other details I should know before I dive in? I'll start generating the meal plan now. Give me a moment to get this sorted for you. |
…689) The redirect approach (load_skill -> redirect -> load_skill for stage) was not working — Gemma 4 E4B does not reliably follow multi-step delegation. The model was loading the monolithic MealPlannerSkill instructions and getting confused. Fix: buildMealPlanContext now returns the session metadata PLUS stage-specific instructions for the current status: collecting_preferences -> ask questions, call save_meal_plan_state high_level_plan_ready -> show plan, call save_meal_plan_state generating_recipes -> generate recipe, call save_meal_plan_state completed -> summarize and close The model follows these instructions directly without needing load_skill. MealPlannerSkill is reduced to a concise 3-line delegate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The generating_recipes stage now instructs the model to: 1. Generate the recipe 2. create_list for the day's recipe list 3. bulk_add_to_list method steps to the recipe list 4. bulk_add_to_list ingredients to the combined shopping list 5. saveMealPlanState to advance the day index Also removed duplicate 'generating_recipes' case and completed the 'completed' stage block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#689) The model was not calling saveMealPlanState after collecting preferences across multiple messages. Instructions now explicitly require: 1. Ask user for preferences 2. IMMEDIATELY call saveMealPlanState with all collected data 3. Generate high-level plan 4. Call saveMealPlanState again with status=high_level_plan_ready Also updated high_level_plan_ready stage to explicitly call saveMealPlanState after showing the plan. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The collecting_preferences stage now includes steps 3-4 to generate a high-level meal plan and save it with status=high_level_plan_ready. This ensures the model transitions through the full flow in one turn after receiving preferences, instead of getting stuck asking questions or calling run_intent with skill names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
All stage-specific skill instructions now use the exact camelCase parameter names (conversationId, peopleCount, days, etc.) that match the @tool annotation on KernelAIToolSet.saveMealPlanState(). This ensures constrained decoding works correctly. Previously instructions used snake_case (save_meal_plan_state, conversation_id, people_count) which caused the model to fail to trigger constrained decoding, resulting in run_intent calls with skill names instead of actual tool invocations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model was calling run_intent with skill names instead of using @tool methods because the injected context block was too verbose (~100 lines of stage instructions) and overwhelmed the model. Now buildMealPlanContext returns only the session metadata block plus a concise directive telling the model to load_skill the appropriate stage-specific skill. This reduces prompt size and gives the model a clear, actionable next step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model reliably ignores the [Tool Use] protocol when the prompt is long, falling back to run_intent with skill names instead of calling loadSkill. This causes the meal planner to fail because the model never reaches the stage-specific skill instructions. Fix: Add a QIR regex pattern for 'plan meals' / 'make a meal plan' etc. that routes to intentName='load_skill' with skill_name= 'meal_planner_collect'. ChatViewModel intercepts this and injects a structured systemContext hint telling the model to call loadSkill(skillName='meal_planner_collect'). Also: reduced buildMealPlanContext to only return session metadata plus a concise load_skill directive, removing the ~100-line stage instructions that were overwhelming the model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model ignores systemContext hints when the prompt is long. The QIR handler that set systemContext and fell through to E4B was unreliable — the model called run_js with old skill names instead of following the hint to call loadSkill. Fix: When QIR matches load_skill → meal_planner_collect, directly execute the load_skill skill to get MealPlannerCollectSkill fullInstructions, then either return as DirectReply (bypassing E4B) or inject into systemContext for the first E4B turn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Test results You: Plan meals How many days are you looking to plan for, and what are your protein preferences (e.g., chicken, fish, vegetarian)? I'll get the high-level plan sorted for you now. I'll start putting together a high-level plan for you can look over. I'll generate a high-level plan for you now. Here is a high-level plan for you, Nick: Day 1:
Day 2:
How does that sound? If you're happy with that, I can start generating the recipes for you. Beef Stir-fry with chicken and vegetables Ingredients:
|
…anState (#689) The model ignores systemContext hints and load_skill directives when the prompt is long. It falls back to run_intent/run_js with learned patterns instead of following stage instructions. Fix: After saveMealPlanState persists state, the skill now returns the next stage skill's fullInstructions as a DirectReply. This bypasses E4B for stage transitions — the model receives the next instructions directly without needing to figure out which skill to load. Stage transitions: collecting_preferences -> Success (model continues collecting) high_level_plan_ready -> DirectReply with meal_planner_plan instructions generating_recipes -> DirectReply with meal_planner_recipe instructions completed -> DirectReply with completion message Also: Updated SaveMealPlanStateSkillTest for new constructor and DirectReply returns on stage transitions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gistry in tests (#689) The model copies placeholder strings (<conv-id>, <N>) from skill schema examples and passes them as literal parameter values. This causes save_meal_plan_state to receive empty/invalid data. Fix: 1. Reject conversation_id values that start with '<' or contain 'session block' — these are clearly placeholders. 2. During Stage 1, reject people_count/days that match placeholder patterns (contain '<' or 'N>'). 3. Remove misleading examples from save_meal_plan_state schema (now emptyList) since they are being copied verbatim by the model. 4. Mock meal_planner_plan and meal_planner_recipe skills in the test so DirectReply stage transitions work in unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…structions (#689) The model ignores systemContext hints and falls back to calling run_intent or run_js with skill names (meal_planner_collect, etc.) instead of using load_skill. Instead of returning errors, intercept these calls and return the skill's fullInstructions directly in the tool response so the model can follow the proper flow. Also remove placeholder examples from all stage skill fullInstructions to prevent the model from copying literal placeholder strings into saveMealPlanState calls. Changes: - KernelAIToolSet.runIntent: reserved skill names now return full instructions instead of error messages - KernelAIToolSet.runJs: same interception for reserved skill names - MealPlannerCollectSkill: removed placeholder saveMealPlanState example - MealPlannerPlanSkill: removed placeholder saveMealPlanState example - MealPlannerRecipeSkill: removed placeholder saveMealPlanState example - MealPlannerCompleteSkill: removed placeholder saveMealPlanState example - ChatViewModel bypass: always DirectReply for meal_planner_collect - Updated MealPlannerPlanSkillTest assertion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Testing results: it got stuck processing and never finished after several minutes. You: "plan a meal for 2 people for 3 days, vegetarian meal_planner_plan: Generate a high-level meal plan for the specified days and preferences. IMPORTANT: Load with load_skill first. Do NOT call run_intent. Only call saveMealPlanState after showing the plan. SESSION CONTEXT BLOCK: At the start of each turn, the system may inject a [Meal Planner Session] block. GENERATE THE PLAN (when preferences are available):
SAVE STATE (critical): FORMATTING: Use METRIC / NZ units only (g, kg, ml, l, tsp, tbsp, Celsius, counts). NEVER use lb, oz, Fahrenheit. Use these instructions to proceed. Do NOT call run_intent again. Here's a 3-day plan for 2 vegetarians: Day 1: Lentil Dahl | Day 2: Vegetable Curry | Day 3: Quinoa Salad Ready for the full recipes with cooking steps? |
…689) Three fixes addressing persistent meal planner instruction-following failures: 1. Fix QIR regex to match 'plan a meal' (user said 'plan a meal for 2 people') - Added optional 'a' before meal/food/dinner/lunch/breakfast in regex 2. Skip collect stage when user already provided preferences - Detect preference keywords (people count, days, dietary restrictions) - Load meal_planner_plan directly instead of meal_planner_collect - Prevents redundant preference-gathering when user already stated them 3. Fix meal_planner_plan fullInstructions - Removed 'Do NOT call run_intent' conflict (model triggered via run_intent) - Made saveMealPlanState call explicit and immediate after plan display - Added concrete example of highLevelPlan JSON structure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase A of the meal-planner redesign per docs/research/meal-planner-skill-design.md:
1. MealPlannerStateMachine: explicit state machine with valid transitions
- States: COLLECTING_PREFERENCES -> PLAN_DRAFT_READY -> PLAN_CONFIRMED ->
GENERATING_RECIPE -> RECIPE_REVIEW -> WRITING_ARTIFACTS -> COMPLETED
- Transitions enforced by code, not the LLM
2. MealPlannerCoordinator: deterministic workflow engine
- Owns flow control, persistence, and stage progression
- Parses user input for preference keywords (people count, days, dietary)
- Delegates content generation to stage skills only when needed
- Thread-safe via Mutex
3. QIR routing: changed from load_skill(meal_planner_collect) to
start_meal_planner intent
4. KernelAIToolSet: added start_meal_planner to reserved skill names
5. MealPlanSessionRepository: added currentDayIndex parameter to
updatePreferences()
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Inject MealPlannerCoordinator from core:skills (fix import path) - Add active-meal-planner detection before QIR routing - Replace load_skill bypass with coordinator.startOrResume() - Add CoordinatorResult.content property to sealed interface - Clean up blank lines from previous ast_edit operations
#689) - Add statusString property to MealPlannerStateMachine.State enum to explicitly map states to DB status strings - Update transitionTo() to use target.statusString instead of target.name.lowercase() which produced incorrect statuses (e.g. generating_recipe vs generating_recipes) - Add @provides binding for MealPlannerCoordinator in SkillsModule (was missing after MealPlannerSkill was removed) - Fix corrupted ?: operators in LoadSkillSkill and ChatViewModel - Remove dead MealPlannerSkill and MealPlannerSkillTest - Clean up stale references in KernelAIToolSet and LoadSkillSkill - Add 27 tests in MealPlannerCoordinatorTest covering state transitions, regex parsing, and session resume logic
LoadSkillSkill.execute() was returning a generic 'Instructions loaded' message without the actual instructions content. The model was told to 'follow them' but could never see the instructions. Now returns the skill's fullInstructions property in the success response so the model can actually read and execute them.
ActionsViewModel.executeIntent was routing start_meal_planner through run_intent → NativeIntentHandler, which doesn't know the intent. Now routes directly to MealPlannerCoordinator.startOrResume() with a generated conversation ID, matching the ChatViewModel path.
Summary
Conversation-scoped, Room-backed meal-planner session state with a deterministic coordinator that owns workflow control, replacing the previous LLM-driven approach that caused infinite loops and stale context leakage.
Architecture
Key changes
New: Deterministic coordinator (
MealPlannerCoordinator)Mutex+StateFlowfor active session trackingNew: Stage-specific skills (loaded via
load_skill)MealPlannerCollectSkill— collects preferencesMealPlannerPlanSkill— generates high-level planMealPlannerRecipeSkill— generates recipe for current dayMealPlannerCompleteSkill— marks session completeMealPlannerSkillreduced to a redirect that tells the model toload_skillthe appropriate stageNew: Room-backed session store
MealPlanSessionEntity— conversation-scoped session stateMealPlanSessionRepository— CRUD operations for session dataChatViewModel wiring
MealPlannerCoordinatorstart_meal_plannerintent from QIR triggers coordinatorstartOrResume()processMessage()buildMealPlanContextprovides structured session metadata for LLM-invoked stagesReserved skill name interception
run_intentandrun_jsintercept reserved skill names and returnfullInstructionsdirectlyrun_intent(meal_planner_plan)which previously caused infinite loopsTesting
./gradlew assembleDebug— builds cleanMemoryRepositoryImplTestandLatexConversionTestare unrelated)Known issues
run_intentwith skill names if it bypasses the coordinator — the intercept returns instructions but the model may ignore themprocessMessagehandles all active-session turns, but the model's tool-calling path is separate from the user-message pathCloses #689