Skip to content

[Bug]: Per-message nudge triggers every turn after restart — lastPerMessageNudgeTokens not persisted when nudge fires without anchor changes #60

Closed

Description

@Lisc1985

Bug Description

Describe the bug

The per-message context nudge fires on every single turn after restarting OpenCode, regardless of actual token growth since the last nudge. The nudge message shows a large growth (e.g. +69.8K since last nudge), but this is actually the total session usage, not real growth since the last nudge.

Root cause

In dist/index.js, when a per-message nudge triggers, the in-memory state is updated:

// ~line 6338
state.nudges.lastPerMessageNudgeTokens = currentTokens;
state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
However, saveSessionState() is only called when anchorsChanged is true:
// ~line 6380
if (anchorsChanged) {
    saveSessionState(state, logger).catch(() => {});
}
When a nudge triggers via the growth check (growthSinceLastNudge >= nudgeGrowthTokens) without any anchor changes (no compress/decompress operations), anchorsChanged remains false, and the updated lastPerMessageNudgeTokens is never persisted to disk.
On the next OpenCode restart:
1. loadSessionState() reads the stale lastPerMessageNudgeTokens from disk (e.g. 73018, set hours/turns ago)
2. currentTokens is now much higher (e.g. 142K)
3. growth = 142K - 73K = 69K >= nudgeGrowthTokens (30000) → nudge triggers
4. In-memory update again not persisted (anchorsChanged = false)
5. Infinite loop: nudge fires every turn for the entire session lifetime
Reproduction
1. Start a long session (1267+ turns, ~142K tokens)
2. Let a nudge fire naturally (growth exceeds nudgeGrowthTokens)
3. Ensure no compress/decompress happens around the same time
4. Restart OpenCode (close and reopen the TUI)
5. Send any message → nudge fires immediately
6. Send another message → nudge fires again
7. Repeat indefinitely
Expected behavior
After a nudge fires, lastPerMessageNudgeTokens should be persisted to disk so that on restart, the growth baseline is correct and the nudge only fires when actual new growth exceeds the threshold.
Environment
- OpenCode version: 1.17.13
- opencode-acp plugin version: 0.19
- OS: win10
Proposed fix
Change the save condition to also persist when a nudge has fired:
if (anchorsChanged || decision.shouldNudge) {
    saveSessionState(state, logger).catch(() => {});
}
(Older versions use shouldNudge instead of decision.shouldNudge — same fix applies.)
Session state evidence
{
  "nudges": {
    "lastPerMessageNudgeTokens": 73018,
    "lastPerMessageNudgeTurn": 1267
  },
  "lastUpdated": "2026-07-06T10:04:24"
}

### Expected Behavior

nudge state能被正确更新。if (anchorsChanged)改为if (anchorsChanged || decision.shouldNudge)后使用,不在每轮收到nudge触发。

### Debug Context Logs

```json
无log,llm上下文有

Tool Call Details

No response

DCP Version

0.19

Opencode Version

1.17.13

Model

Other (specify in description)

Additional Context

No response

Activity

  1. ranxianglei commented on Jul 6, 2026

    @ranxianglei
    Owner

    Thanks for the precise report — your root-cause analysis was spot-on. This is fixed and shipped in opencode-acp@1.9.2 (now latest on npm).

    What was happening

    lastPerMessageNudgeTokens was updated in memory when a growth nudge fired, but saveSessionState() only ran when anchorsChanged === true. A growth nudge doesn't always change anchors (turn/iteration anchor sets saturate once seeded, or the last message is an assistant turn with no user turn to anchor), so the new baseline was never flushed to disk. On restart, the stale baseline was reloaded → growth = currentTokens − staleBaseline exceeded the threshold → the nudge refired every turn. Exactly your reproduction.

    The fix (PR #61, shipped in 1.9.2)

    - if (anchorsChanged) {
    + if (anchorsChanged || decision.shouldNudge) {
          saveSessionState(state, logger).catch(() => {})
      }

    i.e. whenever a nudge actually fires, the new baseline is persisted — same fix you proposed. A regression test was added in tests/inject.test.ts (seeds a stale baseline to disk, fires a growth nudge with anchorsChanged=false, reloads, asserts the persisted baseline advanced).

    Please upgrade

    opencode-acp@1.9.2
    

    After upgrading, the every-turn nudge loop stops: the first nudge after restart will flush the correct baseline, and subsequent nudges only fire on real new growth beyond nudgeGrowthTokens.

    If the issue persists after upgrading, please reopen with the new debug logs. Thanks again for the detailed report — it made this a straightforward fix.

    修复已发布到 opencode-acp@1.9.2(npm latest),请升级验证。你的根因分析完全正确,正是 saveSessionState 的保存守卫漏了 shouldNudge 这一路。升级后重启,第一轮 nudge 即会落盘正确基线,不再每轮重复触发。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions