siyuan-note/siyuan · error

agent session has an uncommitted turn

Error message

agent session has an uncommitted turn

What it means

beginRuntimeTurn refuses to install a new ActiveTurn when the session already has a different ActiveTurn that has not been committed, raising 'agent session has an uncommitted turn'. The runtime serializes turns per session: only one uncommitted turn may be in flight. Once the existing turn is committed, it is cleared and the new turn proceeds.

Source

Thrown at kernel/agent/runtime.go:272

		return fmt.Errorf("invalid session id")
	}
	if turn.TurnID == "" || turn.State != "running" {
		return fmt.Errorf("invalid agent runtime turn")
	}
	lock := sessionLock(sessionID)
	lock.Lock()
	defer lock.Unlock()
	runtime, err := loadRuntimeLocked(sessionID)
	if err != nil {
		return err
	}
	if runtime.ActiveTurn != nil && runtime.ActiveTurn.TurnID != turn.TurnID {
		committed, err := isTurnCommittedLocked(sessionID, runtime.ActiveTurn.TurnID)
		if err != nil {
			return err
		}
		if !committed {
			return fmt.Errorf("agent session has an uncommitted turn")
		}
		runtime.ActiveTurn = nil
	}
	data, err := os.ReadFile(filepath.Join(sessionsDir(), sessionID, "session.json"))
	if err != nil {
		return err
	}
	var session map[string]any
	if err := gulu.JSON.UnmarshalJSON(data, &session); err != nil {
		return err
	}
	if turn.BaseRevision >= 0 && numberToInt64(session["revision"]) != turn.BaseRevision {
		return ErrSessionConflict
	}
	if findRuntimeUserAnchor(session, turn.UserEntryID) < 0 {
		return fmt.Errorf("agent runtime user entry not found")
	}
	runtime.ActiveTurn = turn

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Commit or roll back the existing ActiveTurn (via the session save/commit path) before starting a new turn
  2. Check the session's runtime state before beginning a turn and reject/queue if an uncommitted turn exists
  3. Serialize turn start per session in the caller (e.g. per-session queue or mutex)

Example fix

// before
beginRuntimeTurn(sid, turnB) // fails: turnA uncommitted
// after
if err := commitRuntimeTurn(sid, turnA); err != nil { return err }
beginRuntimeTurn(sid, turnB)
Defensive patterns

Strategy: try-catch

Validate before calling

if runtime.ActiveTurn != nil && runtime.ActiveTurn.TurnID != newTurn.TurnID { ensureCommitted(sessionID, runtime.ActiveTurn.TurnID) }

Try / catch

if err := beginRuntimeTurn(sid, turn); err != nil && strings.Contains(err.Error(), "uncommitted turn") { commitOrRollbackActiveTurn(sid); return beginRuntimeTurn(sid, turn) }

Prevention

When it happens

Trigger: Calling beginRuntimeTurn while a previous turn is still active and isTurnCommittedLocked returns false for it (same sessionID, different TurnID than the incoming turn).

Common situations: Concurrent or overlapping agent requests on the same session; a previous turn crashed without commit/rollback; a client retried before the first request finished.

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@8641553a1f (2026-09-11). Data as JSON: /api/errors/15e7a627317d1004. Report an issue: GitHub.