siyuan-note/siyuan · error
agent session revision conflict
Error message
agent session revision conflict
What it means
ErrSessionConflict is the public sentinel for optimistic-concurrency failures on agent sessions. SaveSessionState returns it when the client's ExpectedRevision does not match the stored session revision, and beginRuntimeTurn returns it when a turn's BaseRevision is stale — guaranteeing writers operate on the latest state or explicitly lose.
Solutions
- Re-read the session (GetSessionState), apply changes to the fresh revision, and retry with the new ExpectedRevision
- Set ExpectedRevision only when you truly need compare-and-swap; omit it for last-write-wins flows where acceptable
- For runtime turns, refresh BaseRevision from the current session before beginning the turn
- Ensure only one client/writer drives a given session at a time
Example fix
// before
meta.ExpectedRevision = &staleRevision // bumped elsewhere meanwhile
SaveSessionState(data) // -> ErrSessionConflict
// after
revision, current, err := loadCurrentSession(id)
if err != nil { return err }
meta.ExpectedRevision = &revision
data = rebaseChanges(data, current)
SaveSessionState(data) Defensive patterns
Strategy: retry
Validate before calling
revision, session, err := loadCurrentSession(id)
if err == nil && meta.ExpectedRevision != nil && *meta.ExpectedRevision != revision { /* refresh before save */ } Type guard
func isSessionConflict(err error) bool { return errors.Is(err, agent.ErrSessionConflict) } Try / catch
revision, data, err := SaveSessionState(payload)
if errors.Is(err, agent.ErrSessionConflict) {
fresh := fetchSession(id)
payload = rebase(payload, fresh)
payload.Meta.ExpectedRevision = &revision
revision, data, err = SaveSessionState(payload) // bounded retry
} Prevention
- Always read-modify-write: base ExpectedRevision on the just-fetched revision
- Limit retries to one rebase cycle to avoid livelock
- Avoid multiple concurrent editors for a single agent session
When it happens
Trigger: Calling SaveSessionState with sessionMeta.ExpectedRevision set to an old revision while another writer already bumped revision; calling beginRuntimeTurn with turn.BaseRevision >= 0 that no longer equals the stored session revision (runtime.go:285).
Common situations: Two browser tabs or two devices editing the same agent session; a retried request carrying an outdated snapshot after a successful first commit; long-running runtime turns finishing after unrelated session updates.
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 runtime turn changed
- attribute view order changed; retry the drag
- workspace changed during asset scan; retry the request
- agent runtime turn is not finalized
- attribute view rows changed; retry the drag
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/96d72df40bab2856.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/agent/session.go:77
Sessions []*SessionIndexItem `json:"sessions"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type sessionMeta struct {
ID string `json:"id"`
Title string `json:"title"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
Revision int64 `json:"revision"`
ExpectedRevision *int64 `json:"expectedRevision,omitempty"`
CommitTurnID string `json:"commitTurnID,omitempty"`
RecoveryTurnID string `json:"recoveryTurnID,omitempty"`
LastCommittedTurnID string `json:"lastCommittedTurnID,omitempty"`
}
var ErrSessionConflict = errors.New("agent session revision conflict")
var ErrRuntimeNotFinalized = errors.New("agent runtime turn is not finalized")
var sessionLocks sync.Map
func sessionLock(id string) *sync.Mutex {
lock, _ := sessionLocks.LoadOrStore(id, &sync.Mutex{})
return lock.(*sync.Mutex)
}
func loadSessionIndex() map[string]*SessionIndexItem {
data, err := os.ReadFile(sessionsIndexPath())
if err != nil {
return nil
}
var index map[string]*SessionIndexItem
if gulu.JSON.UnmarshalJSON(data, &index) != nil {
return nil
}View on GitHub (pinned to 9f775e8a12)