{"record":{"id":"15e7a627317d1004","repo":"siyuan-note/siyuan","slug":"agent-session-has-an-uncommitted-turn","errorCode":null,"errorMessage":"agent session has an uncommitted turn","messagePattern":"agent session has an uncommitted turn","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/agent/runtime.go","lineNumber":272,"sourceCode":"\t\treturn fmt.Errorf(\"invalid session id\")\n\t}\n\tif turn.TurnID == \"\" || turn.State != \"running\" {\n\t\treturn fmt.Errorf(\"invalid agent runtime turn\")\n\t}\n\tlock := sessionLock(sessionID)\n\tlock.Lock()\n\tdefer lock.Unlock()\n\truntime, err := loadRuntimeLocked(sessionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif runtime.ActiveTurn != nil && runtime.ActiveTurn.TurnID != turn.TurnID {\n\t\tcommitted, err := isTurnCommittedLocked(sessionID, runtime.ActiveTurn.TurnID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !committed {\n\t\t\treturn fmt.Errorf(\"agent session has an uncommitted turn\")\n\t\t}\n\t\truntime.ActiveTurn = nil\n\t}\n\tdata, err := os.ReadFile(filepath.Join(sessionsDir(), sessionID, \"session.json\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar session map[string]any\n\tif err := gulu.JSON.UnmarshalJSON(data, &session); err != nil {\n\t\treturn err\n\t}\n\tif turn.BaseRevision >= 0 && numberToInt64(session[\"revision\"]) != turn.BaseRevision {\n\t\treturn ErrSessionConflict\n\t}\n\tif findRuntimeUserAnchor(session, turn.UserEntryID) < 0 {\n\t\treturn fmt.Errorf(\"agent runtime user entry not found\")\n\t}\n\truntime.ActiveTurn = turn","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/agent/runtime.go#L254-L290","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const runtime = loadRuntime(sessionID);\nif (runtime.activeTurn && runtime.activeTurn.turnID !== turn.turnID) {\n  const committed = isTurnCommitted(sessionID, runtime.activeTurn.turnID);\n  if (!committed) throw new Error(\"commit the active turn before starting a new one\");\n}","typeGuard":null,"tryCatchPattern":"if err := beginRuntimeTurn(sessionID, turn); err != nil {\n  if strings.Contains(err.Error(), \"uncommitted turn\") {\n    // run recovery: commit or interrupt the prior turn, then retry once\n    recoverActiveTurn(sessionID)\n    err = beginRuntimeTurn(sessionID, turn)\n  }\n}","preventionTips":["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"],"tags":["agent","turn","state-machine","recovery"],"backgroundTag":"invalid-state-transition","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}