package session import ( "crypto/sha256" "encoding/hex" "fmt" "os" "path/filepath" "time" "git.gopher.ink/mirrors2/AutoZero/llm" ) // Checkpoint 是一次上下文压缩留下的交接文档,以及它覆盖到原 transcript 的哪个位置。 // // 它值得落盘:压缩本身只改视图,但 WebUI 每个回合都会新建 Agent。没有 sidecar, // 新回合又要把整段历史重新摘要一遍;有了它,新回合直接从 checkpoint + 未覆盖的 // 新消息开始。Covered/TailHash 是失效保护——历史被回退或改写后,指纹对不上就 // 当作没有 checkpoint,绝不拿一份覆盖了不同消息的旧摘要继续跑。 type Checkpoint struct { Text string `json:"text"` Covered int `json:"covered"` // 已纳入 checkpoint 的消息条数(transcript 前缀) TailHash string `json:"tail_hash"` // 第 Covered 条消息的指纹 UpdatedAt int64 `json:"updated_at"` } // CheckpointPath 是压缩交接文档的 sidecar 路径(与 .title / .run.json 同目录)。 func CheckpointPath(id string) string { return filepath.Join(Dir(), id+".checkpoint.json") } // LoadCheckpoint 读取 checkpoint;缺失或损坏返回 false。 func LoadCheckpoint(id string) (Checkpoint, bool) { if _, ok := SafeID(id); !ok { return Checkpoint{}, false } var cp Checkpoint if err := LoadState(CheckpointPath(id), &cp); err != nil { return Checkpoint{}, false } return cp, cp.Text != "" } // SaveCheckpoint 原子写 checkpoint。 func SaveCheckpoint(id string, cp Checkpoint) error { if _, ok := SafeID(id); !ok { return os.ErrInvalid } if cp.UpdatedAt == 0 { cp.UpdatedAt = time.Now().UnixMilli() } return SaveState(CheckpointPath(id), cp) } // RemoveCheckpoint 删除 checkpoint sidecar(会话删除时用)。 func RemoveCheckpoint(id string) { if _, ok := SafeID(id); ok { _ = os.Remove(CheckpointPath(id)) } } // MessageFingerprint 是一条消息的稳定指纹,用于识别 checkpoint 的覆盖边界。 // 只取会进模型视野的字段(role/content/tool call),不含本地 trace。 func MessageFingerprint(m llm.Message) string { h := sha256.New() fmt.Fprintf(h, "%s\x00%s\x00%s\x00", m.Role, m.Content, m.ToolCallID) for _, tc := range m.ToolCalls { fmt.Fprintf(h, "%s\x00%s\x00%s\x00", tc.ID, tc.Name, tc.Input) } return hex.EncodeToString(h.Sum(nil)) }