{"record":{"id":"96d72df40bab2856","repo":"siyuan-note/siyuan","slug":"agent-session-revision-conflict","errorCode":null,"errorMessage":"agent session revision conflict","messagePattern":"agent session revision conflict","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/agent/session.go","lineNumber":77,"sourceCode":"\tSessions []*SessionIndexItem `json:\"sessions\"`\n\tTotal    int                 `json:\"total\"`\n\tPage     int                 `json:\"page\"`\n\tPageSize int                 `json:\"pageSize\"`\n}\n\ntype sessionMeta struct {\n\tID                  string `json:\"id\"`\n\tTitle               string `json:\"title\"`\n\tCreatedAt           int64  `json:\"createdAt\"`\n\tUpdatedAt           int64  `json:\"updatedAt\"`\n\tRevision            int64  `json:\"revision\"`\n\tExpectedRevision    *int64 `json:\"expectedRevision,omitempty\"`\n\tCommitTurnID        string `json:\"commitTurnID,omitempty\"`\n\tRecoveryTurnID      string `json:\"recoveryTurnID,omitempty\"`\n\tLastCommittedTurnID string `json:\"lastCommittedTurnID,omitempty\"`\n}\n\nvar ErrSessionConflict = errors.New(\"agent session revision conflict\")\nvar ErrRuntimeNotFinalized = errors.New(\"agent runtime turn is not finalized\")\n\nvar sessionLocks sync.Map\n\nfunc sessionLock(id string) *sync.Mutex {\n\tlock, _ := sessionLocks.LoadOrStore(id, &sync.Mutex{})\n\treturn lock.(*sync.Mutex)\n}\n\nfunc loadSessionIndex() map[string]*SessionIndexItem {\n\tdata, err := os.ReadFile(sessionsIndexPath())\n\tif err != nil {\n\t\treturn nil\n\t}\n\tvar index map[string]*SessionIndexItem\n\tif gulu.JSON.UnmarshalJSON(data, &index) != nil {\n\t\treturn nil\n\t}","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/agent/session.go#L59-L95","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nmeta.ExpectedRevision = &staleRevision // bumped elsewhere meanwhile\nSaveSessionState(data) // -> ErrSessionConflict\n// after\nrevision, current, err := loadCurrentSession(id)\nif err != nil { return err }\nmeta.ExpectedRevision = &revision\ndata = rebaseChanges(data, current)\nSaveSessionState(data)","handlingStrategy":"retry","validationCode":"revision, session, err := loadCurrentSession(id)\nif err == nil && meta.ExpectedRevision != nil && *meta.ExpectedRevision != revision { /* refresh before save */ }","typeGuard":"func isSessionConflict(err error) bool { return errors.Is(err, agent.ErrSessionConflict) }","tryCatchPattern":"revision, data, err := SaveSessionState(payload)\nif errors.Is(err, agent.ErrSessionConflict) {\n    fresh := fetchSession(id)\n    payload = rebase(payload, fresh)\n    payload.Meta.ExpectedRevision = &revision\n    revision, data, err = SaveSessionState(payload) // bounded retry\n}","preventionTips":["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"],"tags":["go","agent","concurrency","optimistic-locking","revision"],"backgroundTag":"invalid-state-transition","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}