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
- Commit or finalize the outstanding active turn (drive its normal completion/recovery path) before beginning a new turn
- If the prior turn is unrecoverable, run the runtime recovery logic that either commits it to history or marks it interrupted
- 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
- Always drive a turn to a terminal committed/interrupted state before starting the next
- Run the session recovery path after crashes or forced shutdowns before issuing new turns
- Serialize turn management per session — avoid two clients driving the same session concurrently
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
- agent runtime user entry not found
- invalid agent runtime turn
- invalid agent runtime turn id
- invalid agent runtime turn state
- agent compaction summary is empty
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 = turnView on GitHub (pinned to 9f775e8a12)