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 index

View on GitHub (pinned to afa823b6b4)

Solutions

  1. Wait for the terminal turn event (SSE turn end / agentRunning=false) before committing
  2. Poll GET /api/ai/agent/getSession until the runtime no longer reports an active non-terminal turn, then resend the same commitTurnID
  3. If the turn is stuck on approval, resolve /api/ai/agent/confirm first
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/7ee5a3b87456c72b. Report an issue: GitHub.