package history // 本文件归属**会话级删除**:`DeleteSession` 及其「删行前先把文档引用读出来」的辅助 // 查询。它从 store.go 拆出来有两条理由:store.go 贴着 500 行硬上限(AGENTS §10), // 而「删一个会话」与「读写消息」本来就是两件事(与 session_persona.go / // session_preview.go / list_after.go 拆出去的理由同一)。 // // 切片 S7 在此加了一条不变量:被删消息携带的文档引用要在**同一个事务**里撤销。 import ( "context" "database/sql" "fmt" ) // DeleteSession 在一个事务中删除一个子会话及其全部消息。固定主会话不可变, // 返回 ErrImmutableMain(design/16 §6)。 // // 被删消息携带的文档引用在**同一个事务**里撤销(切片 S7):会话删掉却留着计数, // 那些文档就永远回收不掉。撤销前先把各行的 document 列读出来(删完就无从再问), // 读列失败或计数写失败都整体回滚 —— 「消息删了、计数没减」同样是 GC 误删的窗口。 func (s *Store) DeleteSession(ctx context.Context, sessionID string) error { if sessionID == MainSessionID { return ErrImmutableMain } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("history: begin delete %s: %w", sessionID, err) } defer func() { _ = tx.Rollback() }() documents, err := sessionDocumentColumns(ctx, tx, sessionID) if err != nil { return fmt.Errorf("history: read documents %s: %w", sessionID, err) } if _, err := tx.ExecContext(ctx, `DELETE FROM chat_messages WHERE session_id = ?`, sessionID); err != nil { return fmt.Errorf("history: delete messages %s: %w", sessionID, err) } if _, err := tx.ExecContext(ctx, `DELETE FROM chat_sessions WHERE id = ? AND type = ?`, sessionID, SessionSub); err != nil { return fmt.Errorf("history: delete session %s: %w", sessionID, err) } for _, raw := range documents { if err := s.releaseDocuments(ctx, tx, raw); err != nil { return err } } if err := tx.Commit(); err != nil { return fmt.Errorf("history: commit delete %s: %w", sessionID, err) } return nil } // sessionDocumentColumns 读出 sessionID 下全部非空 document 列(原始 JSON 文本)。 // 它必须在删行**之前**调用:行一旦删掉,那些引用就再也无从撤销。 func sessionDocumentColumns(ctx context.Context, tx *sql.Tx, sessionID string) ([]string, error) { rows, err := tx.QueryContext(ctx, `SELECT document FROM chat_messages WHERE session_id = ? AND document != ''`, sessionID) if err != nil { return nil, fmt.Errorf("query: %w", err) } defer func() { _ = rows.Close() }() columns := make([]string, 0) for rows.Next() { var raw string if err := rows.Scan(&raw); err != nil { return nil, fmt.Errorf("scan: %w", err) } columns = append(columns, raw) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate: %w", err) } return columns, nil }