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

  1. Re-read the session (GetSessionState), apply changes to the fresh revision, and retry with the new ExpectedRevision
  2. Set ExpectedRevision only when you truly need compare-and-swap; omit it for last-write-wins flows where acceptable
  3. For runtime turns, refresh BaseRevision from the current session before beginning the turn
  4. 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

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


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)