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
- Re-fetch the current ActiveTurn and abort or restart the turn if its TurnID changed
- Check turn currency immediately before save and reload the runtime on conflict
- 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
- Save promptly after each turn step to shrink the conflict window
- Treat superseded turns as aborted rather than forcing a save
- Serialize per-session turn work in one worker
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
- agent session has an uncommitted turn
- agent session revision conflict
- invalid agent runtime turn
- agent runtime user entry not found
- agent runtime turn is not finalized
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/fe12ffc68a10e958.
Report an issue: GitHub.