siyuan-note/siyuan · error
agent runtime turn is not finalized
Error message
agent runtime turn is not finalized
What it means
The agent runtime tracks each model turn in a per-session runtime state; a turn must reach a terminal state before its results can be committed into session.json. SaveSessionState returns ErrRuntimeNotFinalized (kernel/agent/session.go:393) when the submitted commitTurnID matches the active turn but isRuntimeTurnTerminal is still false — i.e. streaming, tool execution, or confirmation for that turn is still in flight. The API surfaces it as HTTP 409 with the current revision.
Source
Thrown at kernel/agent/session.go:77
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 afa823b6b4)
Solutions
- Wait for the terminal turn event (SSE turn end / agentRunning=false) before committing
- Poll GET /api/ai/agent/getSession until the runtime no longer reports an active non-terminal turn, then resend the same commitTurnID
- If the turn is stuck on approval, resolve /api/ai/agent/confirm first
- If the kernel restarted mid-turn, GET /api/ai/agent/getSession first — it runs FinalizeOrphanedTurn so the orphaned turn becomes committable
Example fix
// before: commit on stream end guess
stream.on('close', () => save(payload));
// after: commit only once the turn is terminal
stream.on('turn_end', async () => {
let s = await fetchPost('/api/ai/agent/getSession', {id});
while (s.data.agentRunning) { s = await fetchPost('/api/ai/agent/getSession', {id}); }
await fetchPost('/api/ai/agent/saveSession', {...payload, commitTurnID: turnID});
}); Defensive patterns
Strategy: retry
Validate before calling
// Commit only after the runtime reports the turn finished
const s = await fetchPost('/api/ai/agent/getSession', {id});
const terminal = !s.data.agentRunning; // runtime merged, no active turn
if (terminal) { /* safe to save with commitTurnID */ } Type guard
null
Try / catch
catch (e) {
if (e?.status === 409 && /not finalized/.test(e.data?.msg ?? '')) {
// schedule a re-check after the next turn event; do not force-save over the runtime
}
} Prevention
- Drive commits from the turn-end SSE event, never from a client-side timer or stream-close guess
- Resolve pending confirmations (/api/ai/agent/confirm) before committing the turn
- After a kernel restart, GET the session first so FinalizeOrphanedTurn can finish the orphaned turn
- Never strip commitTurnID to bypass the check — the runtime content would be lost
When it happens
Trigger: POST /api/ai/agent/saveSession with commitTurnID (or recoveryTurnID) for a turn whose ActiveTurn is not terminal: committing while the SSE stream is still emitting, while a tool call awaits user confirmation (/api/ai/agent/confirm), or while a browser capability result (/api/ai/agent/browserCapabilityResult) has not arrived yet.
Common situations: Frontend commits as soon as the stream visually completes but before the turn-end event; a turn paused on approval; timing races between stream close and runtime checkpoint persistence; custom clients that commit immediately after their own timeout instead of the turn event.
Related errors
- agent session revision conflict
- read agent runtime failed: %w
- invalid session id
- invalid session data
- decode session data failed: %w
AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18).
Data as JSON: /api/errors/7ee5a3b87456c72b.
Report an issue: GitHub.