@@ -58,6 +58,29 @@ type SQLiteCheckpointStore struct {
5858 ownsDB bool // true 表示本实例打开的连接,Close 时需释放
5959}
6060
61+ type workspaceFingerprintRow struct {
62+ WorkspaceKey string
63+ FingerprintPayload string
64+ UpdatedAtMS int64
65+ }
66+
67+ // WorkspaceCheckpointState 保存工作区当前权威代码基线与文件指纹。
68+ type WorkspaceCheckpointState struct {
69+ WorkspaceKey string
70+ CurrentCheckpointID string
71+ FingerprintPayload string
72+ UpdatedAt time.Time
73+ }
74+
75+ // RunCheckpointBaseline 保存单次 run 的权威回退基线,供异步 diff 查询跨 run 结束后读取。
76+ type RunCheckpointBaseline struct {
77+ SessionID string
78+ RunID string
79+ CheckpointID string
80+ Drifted bool
81+ UpdatedAt time.Time
82+ }
83+
6184// NewSQLiteCheckpointStore 创建 checkpoint 存储实例。
6285// dbPath 为 session.db 文件路径,可通过 session.DatabasePath 获取。
6386func NewSQLiteCheckpointStore (dbPath string ) * SQLiteCheckpointStore {
@@ -114,6 +137,184 @@ func (s *SQLiteCheckpointStore) ensureDB(ctx context.Context) (*sql.DB, error) {
114137 return db , nil
115138}
116139
140+ // SaveWorkspaceFingerprint 把 workspace 最新指纹保存到 SQLite,用于跨会话/重启后的 drift 检测。
141+ func (s * SQLiteCheckpointStore ) SaveWorkspaceFingerprint (
142+ ctx context.Context ,
143+ workspaceKey string ,
144+ fingerprintPayload string ,
145+ updatedAt time.Time ,
146+ ) error {
147+ if err := ctx .Err (); err != nil {
148+ return err
149+ }
150+ db , err := s .ensureDB (ctx )
151+ if err != nil {
152+ return err
153+ }
154+ if err := ensureWorkspaceFingerprintTable (ctx , db ); err != nil {
155+ return err
156+ }
157+ _ , err = db .ExecContext (ctx , `
158+ INSERT INTO workspace_fingerprints(workspace_key, fingerprint_payload, updated_at_ms)
159+ VALUES(?, ?, ?)
160+ ON CONFLICT(workspace_key) DO UPDATE SET
161+ fingerprint_payload=excluded.fingerprint_payload,
162+ updated_at_ms=excluded.updated_at_ms
163+ ` , workspaceKey , fingerprintPayload , toUnixMillis (updatedAt ))
164+ if err != nil {
165+ return fmt .Errorf ("checkpoint: save workspace fingerprint %s: %w" , workspaceKey , err )
166+ }
167+ return nil
168+ }
169+
170+ // LoadWorkspaceFingerprint 从 SQLite 读取 workspace 最近指纹,不存在时 ok=false。
171+ func (s * SQLiteCheckpointStore ) LoadWorkspaceFingerprint (
172+ ctx context.Context ,
173+ workspaceKey string ,
174+ ) (string , bool , error ) {
175+ if err := ctx .Err (); err != nil {
176+ return "" , false , err
177+ }
178+ db , err := s .ensureDB (ctx )
179+ if err != nil {
180+ return "" , false , err
181+ }
182+ if err := ensureWorkspaceFingerprintTable (ctx , db ); err != nil {
183+ return "" , false , err
184+ }
185+ var row workspaceFingerprintRow
186+ err = db .QueryRowContext (ctx , `
187+ SELECT workspace_key, fingerprint_payload, updated_at_ms
188+ FROM workspace_fingerprints
189+ WHERE workspace_key = ?
190+ ` , workspaceKey ).Scan (& row .WorkspaceKey , & row .FingerprintPayload , & row .UpdatedAtMS )
191+ if err != nil {
192+ if errors .Is (err , sql .ErrNoRows ) {
193+ return "" , false , nil
194+ }
195+ return "" , false , fmt .Errorf ("checkpoint: load workspace fingerprint %s: %w" , workspaceKey , err )
196+ }
197+ return row .FingerprintPayload , true , nil
198+ }
199+
200+ // SaveWorkspaceCheckpointState 持久化工作区当前代码基线与文件指纹。
201+ func (s * SQLiteCheckpointStore ) SaveWorkspaceCheckpointState (ctx context.Context , state WorkspaceCheckpointState ) error {
202+ if err := ctx .Err (); err != nil {
203+ return err
204+ }
205+ if state .WorkspaceKey == "" {
206+ return fmt .Errorf ("checkpoint: workspace key required" )
207+ }
208+ db , err := s .ensureDB (ctx )
209+ if err != nil {
210+ return err
211+ }
212+ if err := ensureWorkspaceCheckpointStateTable (ctx , db ); err != nil {
213+ return err
214+ }
215+ _ , err = db .ExecContext (ctx , `
216+ INSERT INTO workspace_checkpoint_states(workspace_key, current_checkpoint_id, fingerprint_payload, updated_at_ms)
217+ VALUES(?, ?, ?, ?)
218+ ON CONFLICT(workspace_key) DO UPDATE SET
219+ current_checkpoint_id=excluded.current_checkpoint_id,
220+ fingerprint_payload=excluded.fingerprint_payload,
221+ updated_at_ms=excluded.updated_at_ms
222+ ` , state .WorkspaceKey , state .CurrentCheckpointID , state .FingerprintPayload , toUnixMillis (state .UpdatedAt ))
223+ if err != nil {
224+ return fmt .Errorf ("checkpoint: save workspace checkpoint state %s: %w" , state .WorkspaceKey , err )
225+ }
226+ return nil
227+ }
228+
229+ // LoadWorkspaceCheckpointState 读取工作区当前代码基线与文件指纹,不存在时 ok=false。
230+ func (s * SQLiteCheckpointStore ) LoadWorkspaceCheckpointState (ctx context.Context , workspaceKey string ) (WorkspaceCheckpointState , bool , error ) {
231+ if err := ctx .Err (); err != nil {
232+ return WorkspaceCheckpointState {}, false , err
233+ }
234+ db , err := s .ensureDB (ctx )
235+ if err != nil {
236+ return WorkspaceCheckpointState {}, false , err
237+ }
238+ if err := ensureWorkspaceCheckpointStateTable (ctx , db ); err != nil {
239+ return WorkspaceCheckpointState {}, false , err
240+ }
241+ var state WorkspaceCheckpointState
242+ var updatedAtMS int64
243+ err = db .QueryRowContext (ctx , `
244+ SELECT workspace_key, current_checkpoint_id, fingerprint_payload, updated_at_ms
245+ FROM workspace_checkpoint_states
246+ WHERE workspace_key = ?
247+ ` , workspaceKey ).Scan (& state .WorkspaceKey , & state .CurrentCheckpointID , & state .FingerprintPayload , & updatedAtMS )
248+ if err != nil {
249+ if errors .Is (err , sql .ErrNoRows ) {
250+ return WorkspaceCheckpointState {}, false , nil
251+ }
252+ return WorkspaceCheckpointState {}, false , fmt .Errorf ("checkpoint: load workspace checkpoint state %s: %w" , workspaceKey , err )
253+ }
254+ state .UpdatedAt = fromUnixMillis (updatedAtMS )
255+ return state , true , nil
256+ }
257+
258+ // SaveRunCheckpointBaseline 持久化单次 run 的权威回退基线。
259+ func (s * SQLiteCheckpointStore ) SaveRunCheckpointBaseline (ctx context.Context , baseline RunCheckpointBaseline ) error {
260+ if err := ctx .Err (); err != nil {
261+ return err
262+ }
263+ if baseline .SessionID == "" || baseline .RunID == "" {
264+ return fmt .Errorf ("checkpoint: session_id and run_id required for run baseline" )
265+ }
266+ db , err := s .ensureDB (ctx )
267+ if err != nil {
268+ return err
269+ }
270+ if err := ensureRunCheckpointBaselineTable (ctx , db ); err != nil {
271+ return err
272+ }
273+ _ , err = db .ExecContext (ctx , `
274+ INSERT INTO run_checkpoint_baselines(session_id, run_id, checkpoint_id, drifted, updated_at_ms)
275+ VALUES(?, ?, ?, ?, ?)
276+ ON CONFLICT(session_id, run_id) DO UPDATE SET
277+ checkpoint_id=excluded.checkpoint_id,
278+ drifted=excluded.drifted,
279+ updated_at_ms=excluded.updated_at_ms
280+ ` , baseline .SessionID , baseline .RunID , baseline .CheckpointID , boolToInt (baseline .Drifted ), toUnixMillis (baseline .UpdatedAt ))
281+ if err != nil {
282+ return fmt .Errorf ("checkpoint: save run baseline %s/%s: %w" , baseline .SessionID , baseline .RunID , err )
283+ }
284+ return nil
285+ }
286+
287+ // LoadRunCheckpointBaseline 读取单次 run 的权威回退基线,不存在时 ok=false。
288+ func (s * SQLiteCheckpointStore ) LoadRunCheckpointBaseline (ctx context.Context , sessionID , runID string ) (RunCheckpointBaseline , bool , error ) {
289+ if err := ctx .Err (); err != nil {
290+ return RunCheckpointBaseline {}, false , err
291+ }
292+ db , err := s .ensureDB (ctx )
293+ if err != nil {
294+ return RunCheckpointBaseline {}, false , err
295+ }
296+ if err := ensureRunCheckpointBaselineTable (ctx , db ); err != nil {
297+ return RunCheckpointBaseline {}, false , err
298+ }
299+ var baseline RunCheckpointBaseline
300+ var drifted int
301+ var updatedAtMS int64
302+ err = db .QueryRowContext (ctx , `
303+ SELECT session_id, run_id, checkpoint_id, drifted, updated_at_ms
304+ FROM run_checkpoint_baselines
305+ WHERE session_id = ? AND run_id = ?
306+ ` , sessionID , runID ).Scan (& baseline .SessionID , & baseline .RunID , & baseline .CheckpointID , & drifted , & updatedAtMS )
307+ if err != nil {
308+ if errors .Is (err , sql .ErrNoRows ) {
309+ return RunCheckpointBaseline {}, false , nil
310+ }
311+ return RunCheckpointBaseline {}, false , fmt .Errorf ("checkpoint: load run baseline %s/%s: %w" , sessionID , runID , err )
312+ }
313+ baseline .Drifted = drifted != 0
314+ baseline .UpdatedAt = fromUnixMillis (updatedAtMS )
315+ return baseline , true , nil
316+ }
317+
117318// CreateCheckpoint 在单一事务内写入 checkpoint record + session checkpoint。
118319// 事务内完成 record INSERT → session_cp INSERT → record UPDATE(设置 session_checkpoint_ref + status=available)。
119320func (s * SQLiteCheckpointStore ) CreateCheckpoint (ctx context.Context , input CreateCheckpointInput ) (session.CheckpointRecord , error ) {
@@ -663,3 +864,52 @@ func rollbackTx(tx *sql.Tx) {
663864 _ = tx .Rollback ()
664865 }
665866}
867+
868+ func ensureWorkspaceFingerprintTable (ctx context.Context , db * sql.DB ) error {
869+ if db == nil {
870+ return fmt .Errorf ("checkpoint: workspace fingerprint db is nil" )
871+ }
872+ if _ , err := db .ExecContext (ctx , `
873+ CREATE TABLE IF NOT EXISTS workspace_fingerprints (
874+ workspace_key TEXT PRIMARY KEY,
875+ fingerprint_payload TEXT NOT NULL,
876+ updated_at_ms INTEGER NOT NULL
877+ )` ); err != nil {
878+ return fmt .Errorf ("checkpoint: ensure workspace_fingerprints table: %w" , err )
879+ }
880+ return nil
881+ }
882+
883+ func ensureWorkspaceCheckpointStateTable (ctx context.Context , db * sql.DB ) error {
884+ if db == nil {
885+ return fmt .Errorf ("checkpoint: workspace checkpoint state db is nil" )
886+ }
887+ if _ , err := db .ExecContext (ctx , `
888+ CREATE TABLE IF NOT EXISTS workspace_checkpoint_states (
889+ workspace_key TEXT PRIMARY KEY,
890+ current_checkpoint_id TEXT NOT NULL,
891+ fingerprint_payload TEXT NOT NULL,
892+ updated_at_ms INTEGER NOT NULL
893+ )` ); err != nil {
894+ return fmt .Errorf ("checkpoint: ensure workspace_checkpoint_states table: %w" , err )
895+ }
896+ return nil
897+ }
898+
899+ func ensureRunCheckpointBaselineTable (ctx context.Context , db * sql.DB ) error {
900+ if db == nil {
901+ return fmt .Errorf ("checkpoint: run checkpoint baseline db is nil" )
902+ }
903+ if _ , err := db .ExecContext (ctx , `
904+ CREATE TABLE IF NOT EXISTS run_checkpoint_baselines (
905+ session_id TEXT NOT NULL,
906+ run_id TEXT NOT NULL,
907+ checkpoint_id TEXT NOT NULL,
908+ drifted INTEGER NOT NULL,
909+ updated_at_ms INTEGER NOT NULL,
910+ PRIMARY KEY(session_id, run_id)
911+ )` ); err != nil {
912+ return fmt .Errorf ("checkpoint: ensure run_checkpoint_baselines table: %w" , err )
913+ }
914+ return nil
915+ }
0 commit comments