A guided movement-discovery session runs with an on-screen AI facilitator: voice, scripted surfaces, not therapy. After readiness, the guide collects three memories of resisting something the client meant to do. The UI lists them for confirmation, then asks whether they are ready to move on. Later script steps are not written yet, so the honest end state is a prototype stop line.
Anyone testing the memory step with a fast instruction-following model (Flash-class) after the client already said yes to moving on hits the same bug. The facilitator re-shows or re-reads the three memories on the next turns instead of stopping. Sometimes the reply narrates two rules that contradict each other in the same paragraph.
I treated "moved on" as a UI milestone. The database flag flipped, the button disabled, the client saw the next screen. The system prompt still carried the open-step review block (memories_complete / memories_review). While that block exists, the model does what it says: list the three again and ask about readiness. A newer <moved_on> section does not reliably win when the old task text is still in context. Two other guardrails made it worse: prototype_stop must never answer readiness directly, and each bounded block should fire once. The model tries to satisfy all of them at once and talks itself in circles.
Pass movedOn into prompt composition
The Convex chat generate path already knew whether the memory list was complete and whether the client had moved on. That boolean had to drive prompt assembly, not just React state.
When movedOn is false, buildResistanceMemories keeps the open review copy: show the three, confirm, ask about moving on. When movedOn is true, the same builder swaps in reviewMovedOn text and drops reviewOpen entirely. composeChatAgentPrompt(..., { movedOn, saving }) only appends { body: memoriesMovedOn, tag: 'moved_on' } when the flag is set. Crisis routing still renders first. scheduleReply / generateReply pass movedOn from the memory list into that composer on every turn.
The comment in source is blunt: while the review instruction is in the prompt at all, the model follows it over <moved_on>. The reusable rule is ended-step instructions must leave the prompt, not compete with a newer flag.
Prompt map after move-on
If the client signals acute risk, stop the exercise and route to human support.
Ask for three moments they resisted doing something they meant to do. Save each when spoken.
Re-show the three on screen for confirmation: Skipping the gym after a long day; Putting off a hard conversation; Delaying a portfolio review. Ask if they are ready to move on.
Never use the stop line as the answer to readiness. Deliver a bounded block once when the script ends.
Sample next assistant reply
Here are your three again: Skipping the gym after a long day; Putting off a hard conversation; Delaying a portfolio review. Are you ready to move on?
Broken path: review instructions stay in the stack, so a fast model may re-show the three on the next turn.
Toggle the demo. In the memory step, memories_review stays in the map and the sample reply loops the three. After move-on, that section is withheld and <moved_on> drives a single prototype stop.
Sketch of the composer
This is the shape, not a paste from production:
export function composeChatAgentPrompt(ctx: SessionCtx, opts: { movedOn: boolean; saving: boolean }) {
const sections = [crisisRouting(), buildResistanceMemories(opts.saving, opts.movedOn), sessionTone()]
if (opts.movedOn) {
sections.push({ tag: 'moved_on', body: memoriesMovedOn })
}
return renderSections(sections)
}
function buildResistanceMemories(saving: boolean, movedOn: boolean) {
if (movedOn) {
return { task: collectComplete, review: reviewMovedOn }
}
return { task: collectOpen, review: reviewOpen }
}Withholding reviewOpen is the whole fix. Injecting <moved_on> without removing the open review is decoration.
Probes so Flash cannot regress
I added eval probes on the generate path so a Flash-class reply never re-lists all three after move-on. The probe asserts the assistant message does not contain the saved memory strings and that the prototype stop copy appears once. That caught the failure faster than rereading Dutch QA transcripts.
If your product keeps stale task text beside a lifecycle flag, expect the same loop. Drop the ended step from the prompt when the user leaves it. Flags belong in your database and UI. They are not a substitute for deleting instructions the model still reads as live work.
Happy coding! Sander