Skip to content

fix(core): stop forks from replaying superseded instructions - #61551

Open
yyytttwww888-beep wants to merge 2 commits into
anomalyco:devfrom
yyytttwww888-beep:fork-instruction-state
Open

yyytttwww888-beep wants to merge 2 commits into
anomalyco:devfrom
yyytttwww888-beep:fork-instruction-state

Conversation

@yyytttwww888-beep

@yyytttwww888-beep yyytttwww888-beep commented Nov 4, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #61550

Type of change

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

What does this PR do?

Forks inherit the parent's latest instructions, but older system updates can contradict that baseline in the next request. Mark those copied instruction notices as superseded and exclude them from model context. Keep their messages and IDs so fork/revert references remain valid, and preserve other system notices and subsequent instruction updates.

The marker is applied when creating or replaying a fork; existing fork projections are not rewritten automatically.

How did you verify your code works?

  • The historical-fork runner regression fails before the fix and passes afterward.
  • 378 tests passed across runner, projector, creation, compaction, native compaction, instruction-state, and store suites.
  • Fork cases repeated 10 times: 80 passed (bun run script/test.ts test/session-projector.test.ts test/session-runner.test.ts -t fork --rerun-each 10).
  • bun run check passed, including all 35 workspace typecheck tasks.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none">
  <style>@keyframes rules{0%,100%{opacity:.55}50%{opacity:1}}@media(prefers-reduced-motion:no-preference){.rules{animation:rules 2.4s ease-in-out infinite}}</style>
  <g stroke="currentColor"><rect x="8" y="8" width="90" height="56" rx="6" opacity=".5"/><path d="M108 36h24m-6-6 6 6-6 6" opacity=".6"/><rect x="142" y="8" width="90" height="56" rx="6" opacity=".5"/><g class="rules"><path d="M20 41h65M20 49h49M154 41h65M154 49h49"/></g></g>
  <g fill="currentColor" font-family="sans-serif" font-size="11"><text x="20" y="27">Current rules</text><text x="154" y="27">Fork</text></g>
</svg>

Checklist

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

Added by the opencode bot (AI-generated).

Public API: adds an instructionSuperseded metadata key to copied instruction notices in forked sessions (additive, backwards compatible)

@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.

Looks sound. I copied the updated runner and projector tests onto the base: the fork scenarios fail there, with the fork's next request replaying a stale copied instruction update (e.g. "Changed context") while its system prompt already holds the newer inherited baseline ("Latest context"). With this PR the runner, projector, instruction-state and history test files pass (289 pass), and packages/core type-checks.

The approach matches the existing rule that a native compaction checkpoint drops older system notices from the model context. Copied rows keep their IDs, so fork and revert boundaries still work, and instruction changes made after the fork are still delivered (the updated runner test covers this).

Public API: the only change is a new instructionSuperseded: true key in the free-form metadata of copied instruction-notice system messages, which clients reading session messages can see. Additive and backwards compatible. As the description says, forks created before this change aren't rewritten and still replay their stale notices.

No blocking concerns.

@swf-cmd

swf-cmd commented Nov 7, 2026

Copy link
Copy Markdown

One thing this leaves in place: the fork still takes the parent's newest values as its baseline. Whenever the parent had an instruction update, the fork's system prompt differs from the one the parent's history was cached with, so the fork's first request writes the whole conversation to the prompt cache again.

An alternative that fixes the same contradiction and keeps the cache: start the fork from the parent's baseline and keep the copied updates, which then stay valid, and restate whatever changed after the copy at the tail. The fork's prompt then matches the parent's cached prefix. The same idea covers the undo case in #62027. The change is here: dev...swf-cmd:opencode-upstream:fork-instruction-baseline

I can't open it as a PR right now because I'm at the repository's open-PR limit, but I'm happy to once maintainers say which direction they prefer.

@yyytttwww888-beep

Copy link
Copy Markdown
Author

@swf-cmd Thanks, preserving the cached prompt prefix makes sense. I checked your alternative at 44fff3b with the runner harness and found two boundaries where the next request still contains a stale notice without a corrective update:

  • A → B → A, then a historical fork that copies the B notice but excludes the later A notice. The inherited state falls back to A, so observing A emits no update even though B remains in the request.
  • A → B → C, then a native checkpoint, followed by a fork before that checkpoint that still copies B. The fork inherits baseline C, but the checkpoint is absent and B is replayed without a C correction.

I added the A → B → A case and the checkpoint case in 192ba5d8. Both pass here; the four related test files pass (298 tests), each new case passes 10 repetitions, and bun run check passes.

Keeping the prefix looks worth pursuing, but the tracked state needs to match what the retained notices actually express, including the applicable instruction epoch, before computing the next delta.

AI-assisted testing and analysis.

@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 new commit only adds two runner scenarios: forking after the instructions return to their initial value, and forking before a native checkpoint. The fix is unchanged and still holds. I ran the #61550 reproduction on the base, where the fork replays the copied "Changed context" update, and with this PR, where nothing is replayed. The runner, projector, instruction-state and session-instructions tests pass (293), and packages/core type-checks. The public API is the same as in my previous review. No new concerns.

@swf-cmd

swf-cmd commented Nov 7, 2026

Copy link
Copy Markdown

You're right on both, thanks for running them. In the A → B → A case the fork falls back to A while the copied B notice is still in the request. Before a checkpoint it takes the post-checkpoint baseline, which the copied history was never sent with, so B again goes uncorrected.

Keeping the prefix would need the fork to rebuild its state from the copied notices on the baseline of the epoch they belong to, and instruction_state only keeps the current epoch's baseline. That's a bigger change than this fix, so please treat my alternative as withdrawn; it shouldn't hold this PR up. If I come back to keeping the prefix, it'll be a separate follow-up after this lands.

This branch has not been deployed

No deployments
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.

Historical forks replay instruction updates that contradict their inherited baseline

2 participants