Skip to content

fix(core): keep plan mode reminders in sync with session agent - #42652

Merged
rekram1-node merged 4 commits into
v2from
fix-plan-mode
Aug 16, 2026
Merged

rekram1-node merged 4 commits into
v2from
fix-plan-mode

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The plan plugin communicates Plan mode to the model via <system-reminder> synthetic messages injected on session.created / session.agent.selected. Those messages live in the transcript, but the mode itself lives in durable session state (SessionTable.agent) — and several operations edit the transcript without touching the agent:

  • Compaction cuts model-visible history at the checkpoint (SessionHistory.messageRows loads seq >= compaction.seq), dropping the enter reminder while the session stays on plan.
  • Committed reverts delete all message rows past the boundary while SessionTable.agent stays put and no session.agent.selected re-fires. Undoing past the switch into plan leaves the model unaware it's in Plan mode (including the undo-to-empty-session case); undoing past the switch out of plan leaves a stale enter reminder so the model wrongly refuses to edit.
  • Synthetics admitted while a revert is staged never wake execution and are deleted by the commit before delivery.

In all cases the tool hook still hard-blocks edits, but the model has a wrong or missing idea of what mode it is in.

Fix

Reconcile the transcript against the durable agent on every model request via ctx.session.hook("context"):

  • Find the most recent live enter/leave reminder in the visible messages (whole-message equality, so reminder text quoted inside a compaction checkpoint doesn't count).
  • If the session is on plan with no live enter reminder, or off plan with a stale one, insert the correct reminder into the current request — before the trailing user message, matching where agent-switch reminders land — and persist a durable copy via ctx.session.synthetic so subsequent requests find it and the reconciler goes back to a no-op.

The steady state is unchanged: durable enter/leave synthetics on agent switches do the work, and the reconciler does nothing while transcript and agent agree. Reminders only ever land near the request tail and earlier messages are never rewritten, so the prompt cache prefix stays warm; healing costs one small uncached insertion, once.

This matches how Claude Code and Grok handle mode reminders (persisted, append-only, re-derived from mode state at prompt time); their periodic full/sparse re-assertion cadence is intentionally not adopted.

Tests

test/plugin/plan.test.ts runs the real plugin effect against a stubbed plugin context:

  • enter/leave injection on agent switches (pre-existing behavior)
  • missing enter reminder reconciled into the request (undo-to-empty scenario) and persisted
  • steady-state no-op with a live enter reminder
  • stale enter reconciled with a leave reminder
  • no injection for non-plan sessions without plan history
  • no injection when a leave reminder already follows the enter
  • reminder text quoted inside a checkpoint-like message treated as not live

Reminder texts in tests are derived by invoking the plugin rather than duplicating the strings.

@rekram1-node rekram1-node changed the title fix(core): restore plan mode reminder after compaction fix(core): keep plan mode reminders in sync with session agent Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant