siyuan-note/siyuan · error

agent session has an uncommitted turn

Error message

agent session has an uncommitted turn

What it means

Before overwriting the session's ActiveTurn, beginRuntimeTurn checks whether a different existing active turn has been committed to history (isTurnCommittedLocked). If the previous turn was never committed, starting a new turn would silently drop its pending work, so the operation is refused with this error.

Solutions

  1. Commit or finalize the outstanding active turn (drive its normal completion/recovery path) before beginning a new turn
  2. If the prior turn is unrecoverable, run the runtime recovery logic that either commits it to history or marks it interrupted
  3. Verify TurnID — the error only fires for a different turn; if the same turn is being retried, reuse its TurnID
Defensive patterns

Strategy: try-catch

Validate before calling

const runtime = loadRuntime(sessionID);
if (runtime.activeTurn && runtime.activeTurn.turnID !== turn.turnID) {
  const committed = isTurnCommitted(sessionID, runtime.activeTurn.turnID);
  if (!committed) throw new Error("commit the active turn before starting a new one");
}

Try / catch

if err := beginRuntimeTurn(sessionID, turn); err != nil {
  if strings.Contains(err.Error(), "uncommitted turn") {
    // run recovery: commit or interrupt the prior turn, then retry once
    recoverActiveTurn(sessionID)
    err = beginRuntimeTurn(sessionID, turn)
  }
}

Prevention

When it happens

Trigger: Calling beginRuntimeTurn (via the public turn-start path) while the session still has an ActiveTurn with a different TurnID that has not been committed — e.g. an earlier turn crashed mid-run, was interrupted, or its commit step never executed.

Common situations: Recovery after a kernel crash or forced exit during a turn; a client starting a new turn without finishing/committing the previous one; concurrent clients racing to drive the same session.

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/15e7a627317d1004. Report an issue: GitHub.

Appendix: 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 9f775e8a12)