Skip to content

fix(core): hide instruction updates a revert left behind - #63667

Merged
rekram1-node merged 1 commit into
devfrom
stale-instruction-updates
Nov 13, 2026
Merged

rekram1-node merged 1 commit into
devfrom
stale-instruction-updates

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Issue for this PR

Closes #62027

Related: #61550 is the same kind of contradiction in historical forks. This PR doesn't change forks, so that one stays open.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

After a revert, the model could see an instruction update that contradicts its own system prompt.

When a committed revert removes an instruction update, core throws away the instruction state, and the next step builds the system prompt again from current values. Updates that came before the revert point stay in history, though, and the model still sees them. So it gets the fresh system prompt plus an old update saying something else.

Reproduced on dev with the runner test harness:

First → source turns on ("Entered mode") → Second
      → source removed → Third → revert to Second → Fourth

before: system prompt: no "Entered mode"
        messages: First, "Entered mode", Fourth
after:  messages: First, Fourth

This affects every instruction source: MCP guidance, Code Mode catalog, date, API entries. It would also affect a Plan-style "you are in Plan mode" source: after this revert the session is back on build, but the model is told it's in Plan.

The fix: the instruction state already records where its current system prompt starts (epoch_start), and every update before that point is already included in it. History now hides instruction update messages older than that point. If there is no state, as right after the revert clears it, the next system prompt is built fresh, so every older update is hidden.

The filter is in messageEntries, so the runner, generate, compaction, the compaction precompute and session.context all see the same history.

What else changes, and why it's safe:

  • Only sessions where a revert cleared the state behave differently. In a normal session the epoch is the first instruction event, so nothing is hidden. Forks start their epoch at 0, so they're unchanged too.
  • The prompt cache. Updates are only hidden when the system prompt is being rebuilt anyway. The one exception is sessions already in this broken state when this ships: their history shrinks once, which costs one cache miss.
  • After compaction, updates before the checkpoint were already out of the window. When history falls back to an earlier checkpoint, the updates between the 2 checkpoints are now hidden too. The current system prompt already includes them, which matches what native compaction already does.
  • Clients still get every message. The timeline doesn't read this path. Only the model context and session.context (documented as the active context messages) drop the stale update.
  • Cost: one extra lookup of the session's instruction-state row by primary key per history read.

How did you verify your code works?

  • New scenario hides an update a revert left behind once the source is removed, and the existing resyncs the instruction state when a committed revert removes an update now also checks the messages. Both fail without the fix and pass with it.
  • bun typecheck in packages/core.
  • Ran every core test file that touches history, compaction, forks, reverts or instructions (147 files): 2,899 pass. The 34 failures are in 9 shell, integration, provider and image test files, and they fail the same way on unchanged dev on my machine.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix holds up. With the runner harness I reproduced the stale instruction notice after a committed revert on dev, using both the PR's scenarios and the steps from #62027, and it's gone with this change. The session-* core tests pass (794/0), as does tsc -b in packages/core.

Hiding updates whenever no state exists is sound, because the next step always starts a fresh state from a new event. After compaction the hidden updates are already in the baseline, since advancing the epoch copies the current values into it. Forks are unaffected because their epoch starts at 0.

Public API unchanged: the only addition is the internal InstructionState.epochStart. session.context returns fewer messages only in the post-revert case, which matches its documented meaning (the active context messages).

No blocking concerns.

Fixes #62027
Related: #61550

@rekram1-node
rekram1-node merged commit b645291 into dev Nov 13, 2026
15 checks passed
@rekram1-node
rekram1-node deleted the stale-instruction-updates branch November 13, 2026 06:08
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.

Undo can replay stale instruction notices after rebuilding the current baseline

1 participant