siyuan-note/siyuan · error
agent runtime turn is not finalized
Error message
agent runtime turn is not finalized
What it means
ErrRuntimeNotFinalized is returned when a session commit tries to fold in an ActiveTurn that is still running. In SaveSessionState, if the session data carries a runtime whose ActiveTurn is not in a terminal state, the commit is rejected so partially generated assistant output is never persisted as history.
Solutions
- Wait for the runtime turn to reach a terminal state (completed/failed/cancelled) before calling SaveSessionState
- Strip or defer the runtime ActiveTurn from the snapshot if you only want to persist non-turn state
- Poll the runtime state or subscribe to turn-completion events before committing
Example fix
// before
SaveSessionState(data) // runtime.ActiveTurn still running
// after
if runtime.ActiveTurn != nil && !isTerminal(runtime.ActiveTurn) {
return // wait for turn completion first
}
SaveSessionState(data) Defensive patterns
Strategy: try-catch
Validate before calling
if session.Runtime != nil && session.Runtime.ActiveTurn != nil && !isTerminal(session.Runtime.ActiveTurn) { /* defer save */ } Type guard
func turnIsTerminal(t *agentRuntimeTurn) bool { return t != nil && (t.Status == "completed" || t.Status == "failed" || t.Status == "cancelled") } Try / catch
if _, _, err := SaveSessionState(data); errors.Is(err, agent.ErrRuntimeNotFinalized) {
// wait for turn completion event, then retry the save
} Prevention
- Persist session snapshots only after the active turn reaches a terminal state
- On page unload/crash recovery, let the recovery path (recoveryTurnID) finish before full saves
- Subscribe to turn completion events instead of polling saves during streaming
When it happens
Trigger: Calling SaveSessionState with session data whose embedded runtime.ActiveTurn fails isRuntimeTurnTerminal — i.e. committing while the assistant turn is still streaming/running; also reachable via saveSessionContract. The test at session_test.go:324 shows committing recovered data with a running turn returns ErrRuntimeNotFinalized and leaves revision unchanged.
Common situations: Client saves the session snapshot mid-generation (page reload, crash recovery, autosave during streaming); recovery replay persists a turn before its terminal event arrived.
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
- agent session revision conflict
- capability ID collision
- capability is disabled or no longer available
- capability model name collision
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/7ee5a3b87456c72b.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/agent/session.go:78
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
}
return indexView on GitHub (pinned to 9f775e8a12)