{"record":{"id":"7ee5a3b87456c72b","repo":"siyuan-note/siyuan","slug":"agent-runtime-turn-is-not-finalized","errorCode":null,"errorMessage":"agent runtime turn is not finalized","messagePattern":"agent runtime turn is not finalized","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/agent/session.go","lineNumber":78,"sourceCode":"\tTotal    int                 `json:\"total\"`\n\tPage     int                 `json:\"page\"`\n\tPageSize int                 `json:\"pageSize\"`\n}\n\ntype sessionMeta struct {\n\tID                  string `json:\"id\"`\n\tTitle               string `json:\"title\"`\n\tCreatedAt           int64  `json:\"createdAt\"`\n\tUpdatedAt           int64  `json:\"updatedAt\"`\n\tRevision            int64  `json:\"revision\"`\n\tExpectedRevision    *int64 `json:\"expectedRevision,omitempty\"`\n\tCommitTurnID        string `json:\"commitTurnID,omitempty\"`\n\tRecoveryTurnID      string `json:\"recoveryTurnID,omitempty\"`\n\tLastCommittedTurnID string `json:\"lastCommittedTurnID,omitempty\"`\n}\n\nvar ErrSessionConflict = errors.New(\"agent session revision conflict\")\nvar ErrRuntimeNotFinalized = errors.New(\"agent runtime turn is not finalized\")\n\nvar sessionLocks sync.Map\n\nfunc sessionLock(id string) *sync.Mutex {\n\tlock, _ := sessionLocks.LoadOrStore(id, &sync.Mutex{})\n\treturn lock.(*sync.Mutex)\n}\n\nfunc loadSessionIndex() map[string]*SessionIndexItem {\n\tdata, err := os.ReadFile(sessionsIndexPath())\n\tif err != nil {\n\t\treturn nil\n\t}\n\tvar index map[string]*SessionIndexItem\n\tif gulu.JSON.UnmarshalJSON(data, &index) != nil {\n\t\treturn nil\n\t}\n\treturn index","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/agent/session.go#L60-L96","documentation":"ErrRuntimeNotFinalized is returned when a session commit tries to fold in an ActiveTurn that is still running. In SaveSessionState, if the session data carries a runtime whose ActiveTurn is not in a terminal state, the commit is rejected so partially generated assistant output is never persisted as history.","triggerScenarios":"Calling SaveSessionState with session data whose embedded runtime.ActiveTurn fails isRuntimeTurnTerminal — i.e. committing while the assistant turn is still streaming/running; also reachable via saveSessionContract. The test at session_test.go:324 shows committing recovered data with a running turn returns ErrRuntimeNotFinalized and leaves revision unchanged.","commonSituations":"Client saves the session snapshot mid-generation (page reload, crash recovery, autosave during streaming); recovery replay persists a turn before its terminal event arrived.","solutions":["Wait for the runtime turn to reach a terminal state (completed/failed/cancelled) before calling SaveSessionState","Strip or defer the runtime ActiveTurn from the snapshot if you only want to persist non-turn state","Poll the runtime state or subscribe to turn-completion events before committing"],"exampleFix":"// before\nSaveSessionState(data) // runtime.ActiveTurn still running\n// after\nif runtime.ActiveTurn != nil && !isTerminal(runtime.ActiveTurn) {\n    return // wait for turn completion first\n}\nSaveSessionState(data)","handlingStrategy":"try-catch","validationCode":"if session.Runtime != nil && session.Runtime.ActiveTurn != nil && !isTerminal(session.Runtime.ActiveTurn) { /* defer save */ }","typeGuard":"func turnIsTerminal(t *agentRuntimeTurn) bool { return t != nil && (t.Status == \"completed\" || t.Status == \"failed\" || t.Status == \"cancelled\") }","tryCatchPattern":"if _, _, err := SaveSessionState(data); errors.Is(err, agent.ErrRuntimeNotFinalized) {\n    // wait for turn completion event, then retry the save\n}","preventionTips":["Persist session snapshots only after the active turn reaches a terminal state","On page unload/crash recovery, let the recovery path (recoveryTurnID) finish before full saves","Subscribe to turn completion events instead of polling saves during streaming"],"tags":["go","agent","session","lifecycle","commit"],"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"}