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

  1. Wait for the runtime turn to reach a terminal state (completed/failed/cancelled) before calling SaveSessionState
  2. Strip or defer the runtime ActiveTurn from the snapshot if you only want to persist non-turn state
  3. 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

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


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 index

View on GitHub (pinned to 9f775e8a12)