package session import ( "encoding/json" "os" "path/filepath" "strings" ) // RunState 是一次回合的**落盘**状态,供 UI 在进程重启后说明"上次那次跑到哪了"。 // // 注意它能表达的东西有边界:agent 在进程内,进程重启无法续跑。所以这个文件的 // 用途只有一个——把上次遗留的 running 认出来是 interrupted,而不是假装它还在跑。 // 会话消息本身是逐条落盘的(见 Session.Append),不会因为重启丢失。 type RunState struct { RunID string `json:"run_id"` Status string `json:"status"` // running | done | error | stopped | interrupted StartedAt int64 `json:"started_at"` EndedAt int64 `json:"ended_at,omitempty"` Error string `json:"error,omitempty"` } // RunStatePath 运行状态 sidecar 路径(与 TitlePath 同一套命名与目录)。 func RunStatePath(id string) string { return filepath.Join(Dir(), id+".run.json") } // LoadRunState 读取运行状态;文件不存在或损坏时返回 (zero, false)。 func LoadRunState(id string) (RunState, bool) { b, err := os.ReadFile(RunStatePath(id)) if err != nil { return RunState{}, false } var st RunState if json.Unmarshal(b, &st) != nil { return RunState{}, false } return st, true } // SaveRunState 原子写运行状态。 func SaveRunState(id string, st RunState) error { return SaveState(RunStatePath(id), st) } // ReconcileRunStates 把所有遗留的 running 状态改写成 interrupted,返回改写条数。 // // 进程刚起来时调用:此时不可能有任何回合在跑,文件里写着 running 的一定是上次 // 进程退出/被杀时留下的。不这么做的话,UI 会永远显示"正在回复…"。 func ReconcileRunStates() int { entries, err := os.ReadDir(Dir()) if err != nil { return 0 } n := 0 for _, e := range entries { name := e.Name() if e.IsDir() || !strings.HasSuffix(name, ".run.json") { continue } path := filepath.Join(Dir(), name) b, err := os.ReadFile(path) if err != nil { continue } var st RunState if json.Unmarshal(b, &st) != nil || st.Status != "running" { continue } st.Status = "interrupted" if err := SaveState(path, st); err == nil { n++ } } return n }