siyuan-note/siyuan · error

agent runtime turn changed

Error message

agent runtime turn changed

What it means

saveRuntimeTurn detects that the session's ActiveTurn has a different TurnID than the turn being saved and returns 'agent runtime turn changed'. This optimistic-concurrency check prevents overwriting a newer turn's state with stale writes.

Source

Thrown at kernel/agent/runtime.go:328

	if !isValidSessionID(sessionID) {
		return fmt.Errorf("invalid session id")
	}
	lock := sessionLock(sessionID)
	lock.Lock()
	defer lock.Unlock()
	committed, err := isTurnCommittedLocked(sessionID, turn.TurnID)
	if err != nil {
		return err
	}
	if committed {
		return nil
	}
	runtime, err := loadRuntimeLocked(sessionID)
	if err != nil {
		return err
	}
	if runtime.ActiveTurn != nil && runtime.ActiveTurn.TurnID != turn.TurnID {
		return fmt.Errorf("agent runtime turn changed")
	}
	turn.UpdatedAt = time.Now().UnixMilli()
	runtime.ActiveTurn = turn
	return writeRuntimeLocked(sessionID, runtime)
}

func saveRuntimeCompaction(sessionID string, compaction *runtimeCompaction) error {
	if sessionID == "" || compaction == nil {
		return errContextCannotBeCompacted
	}
	if !isValidSessionID(sessionID) {
		return fmt.Errorf("invalid session id")
	}
	lock := sessionLock(sessionID)
	lock.Lock()
	defer lock.Unlock()
	runtime, err := loadRuntimeLocked(sessionID)
	if err != nil {

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Re-fetch the current ActiveTurn and abort or restart the turn if its TurnID changed
  2. Check turn currency immediately before save and reload the runtime on conflict
  3. Serialize turn lifecycle per session so a save cannot race a turn replacement

Example fix

// before
saveRuntimeTurn(sid, oldTurn) // ActiveTurn is now newTurn
// after
runtime, _ := loadRuntime(sid)
if runtime.ActiveTurn != nil && runtime.ActiveTurn.TurnID == oldTurn.TurnID {
    saveRuntimeTurn(sid, oldTurn)
}
Defensive patterns

Strategy: retry

Validate before calling

if runtime.ActiveTurn == nil || runtime.ActiveTurn.TurnID != turn.TurnID { reloadOrRestartTurn() }

Try / catch

if err := saveRuntimeTurn(sid, turn); err != nil && strings.Contains(err.Error(), "turn changed") { turn = reloadTurn(sid, turn.TurnID); if turn == nil { return errTurnSuperseded } }

Prevention

When it happens

Trigger: Saving turn A after the session's ActiveTurn was replaced by turn B (beginRuntimeTurn with a new turn, recovery, or regenerate occurred in between).

Common situations: Long-running request finishing its save after the user regenerated/recovered; retry logic replaying an old save against a session whose turn already advanced; concurrent writers to the same session.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/fe12ffc68a10e958. Report an issue: GitHub.