Skip to content

fix(session): record Plan reminders durably instead of admitting them to the inbox - #62633

Open
tomwangcn wants to merge 1 commit into
anomalyco:devfrom
tomwangcn:fix/plan-reminder-compaction-loop
Open

tomwangcn wants to merge 1 commit into
anomalyco:devfrom
tomwangcn:fix/plan-reminder-compaction-loop

Conversation

@tomwangcn

@tomwangcn tomwangcn commented Nov 7, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #62628

Type of change

  • Bug fix

What does this PR do?

With the plan agent, once auto-compaction triggers, the session loops forever and opencode run --agent plan never exits.

Root cause: the Plan plugin's context hook re-injects the plan-mode reminder whenever compaction (or a committed revert) strips it from history, and persists the copy through ctx.session.synthetic. That admits a pending steer item to the session inbox, and at the next idle boundary the inbox promotes it into a fresh turn whose only job is answering the reminder. With a small enough context limit every answer triggers another compaction, so the cycle sustains itself.

Fix: publish the reminder straight to the durable event log (SessionEvent.Synthetic), the same path instruction-file injection already uses, instead of admitting it as pending inbox input. The reminder still lands in history and is still spliced into the current request's messages, but it no longer spawns an answer turn, so the loop cannot sustain itself. The agent-switch path (session.created / session.agent.selected) still goes through ctx.session.synthetic, so reminder delivery on agent switches is unchanged.

Overlap with #58287 / #58395: #58287 (extra unprompted turn under default_agent: "plan") shares the same mechanism - a synthetic reminder is admitted as pending input and promoted into an unprompted turn - so this change also stops it (verified locally: with default_agent: "plan" and no compaction, 3 requests on dev vs 2 with this PR). #58395 was opened earlier and fixes #58287 a different way; the auto-compaction loop in #62628 is specific to this PR. Happy to reconcile whichever way maintainers prefer.

How did you verify your code works?

  • Mock-model repro from the issue, plan agent with a tiny context limit: before, the process was killed after 40s / ~2,000 requests; after, it exits cleanly after 3 requests (title, compaction, answer). build agent is unchanged (3 requests).
  • default_agent: "plan" config, no --agent flag, large context: 3 requests on dev (extra unprompted answer turn), 2 with this PR.
  • bun test test/plugin/plan.test.ts - 12/12 pass (updated: the context hook now publishes to the durable log rather than admitting to the inbox).
  • bun run typecheck (packages/core) - clean.

Checklist

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

Added by the opencode bot (AI-generated).

Also addresses #58287 (see overlap note above; verified locally 3 -> 2 requests)

Related: #57013

… to the inbox

The Plan plugin's context hook re-injects its reminder whenever compaction
or a committed revert strips it, persisting the copy through
ctx.session.synthetic. That admits a pending steer item, so the next idle
boundary promotes it into a fresh turn answering the reminder again. Once
auto-compaction keeps firing (small context limit), each turn re-strips
and re-admits the reminder and 'opencode run --agent plan' never exits.

Publish the reminder straight onto the durable log instead (the same
path instruction injection uses): it lands in history without creating
pending input, so reconciliation no longer spawns answer turns.

Fixes anomalyco#62628
@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Nov 7, 2026

@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 change works. I ran the #62628 mock-model repro from source. On the base, opencode run --agent plan with a 3000-token context was still looping when the timeout killed it after 40 s and about 4,000 requests. With this PR it exits after 3 requests (title, compaction, answer). Because this hook also handles the first request under default_agent: "plan", the same change stops the extra unprompted turn from #58287: 2 requests instead of 3, and the answer is printed once. plan.test.ts passes and tsc -b is clean for packages/core.

Public API: unchanged. This is an internal change to the built-in Plan plugin; no plugin API, events, config or HTTP surface is touched.

Publishing SessionEvent.Synthetic directly matches what instruction loading and the runner's continuation messages already do, and built-in plugins already depend on Bus.Service. No blocking concerns. One note for maintainers: #58395 is open and fixes #58287 a different way, by adding a prompt-path hook in plan.ts. With this PR that hook may no longer be needed, so the two should be reconciled.

Fixes #58287
Related: #57013

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

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

1 participant