Remove snapshot/restore functionality from compaction

Snapshots defeated the entire purpose of compaction - if we're keeping
the original content, we're not actually saving any space. Compaction
is about graceful memory decay for agentic databases, not reversible
compression.

Removed:
- CreateSnapshot/GetSnapshots/RestoreFromSnapshot from storage
- --restore flag and functionality from bd compact command
- All snapshot-related tests
- Snapshot struct and related code

The database is ephemeral and meant to decay over time. Compaction
actually reduces database size now.

Closes bd-260 (won't fix - conceptually wrong)
Closes bd-261 (already done in bd-259)
This commit is contained in:
Steve Yegge
2025-10-16 00:26:22 -07:00
parent 7fe6bf3e1d
commit da5493bac0
7 changed files with 291 additions and 510 deletions

File diff suppressed because one or more lines are too long

View File

@@ -12,15 +12,14 @@ import (
)
var (
compactDryRun bool
compactTier int
compactAll bool
compactID string
compactForce bool
compactBatch int
compactWorkers int
compactStats bool
compactRestore string
compactDryRun bool
compactTier int
compactAll bool
compactID string
compactForce bool
compactBatch int
compactWorkers int
compactStats bool
)
var compactCmd = &cobra.Command{
@@ -41,7 +40,6 @@ Examples:
bd compact --id bd-42 # Compact specific issue
bd compact --id bd-42 --force # Force compact (bypass checks)
bd compact --stats # Show statistics
bd compact --restore bd-42 # Restore from snapshot
`,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
@@ -57,11 +55,6 @@ Examples:
return
}
if compactRestore != "" {
runCompactRestore(ctx, sqliteStore, compactRestore)
return
}
if compactID != "" && compactAll {
fmt.Fprintf(os.Stderr, "Error: cannot use --id and --all together\n")
os.Exit(1)
@@ -367,11 +360,6 @@ func runCompactStats(ctx context.Context, store *sqlite.SQLiteStorage) {
}
}
func runCompactRestore(ctx context.Context, store *sqlite.SQLiteStorage, issueID string) {
fmt.Fprintf(os.Stderr, "Error: --restore not yet implemented\n")
os.Exit(1)
}
func progressBar(current, total int) string {
const width = 40
if total == 0 {
@@ -398,7 +386,6 @@ func init() {
compactCmd.Flags().IntVar(&compactBatch, "batch-size", 10, "Issues per batch")
compactCmd.Flags().IntVar(&compactWorkers, "workers", 5, "Parallel workers")
compactCmd.Flags().BoolVar(&compactStats, "stats", false, "Show compaction statistics")
compactCmd.Flags().StringVar(&compactRestore, "restore", "", "Restore issue from snapshot")
rootCmd.AddCommand(compactCmd)
}

View File

@@ -88,10 +88,6 @@ func (c *Compactor) CompactTier1(ctx context.Context, issueID string) error {
return fmt.Errorf("dry-run: would compact %s (original size: %d bytes)", issueID, originalSize)
}
if err := c.store.CreateSnapshot(ctx, issue, 1); err != nil {
return fmt.Errorf("failed to create snapshot: %w", err)
}
summary, err := c.haiku.SummarizeTier1(ctx, issue)
if err != nil {
return fmt.Errorf("failed to summarize with Haiku: %w", err)
@@ -235,10 +231,6 @@ func (c *Compactor) compactSingleWithResult(ctx context.Context, issueID string,
result.OriginalSize = len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
if err := c.store.CreateSnapshot(ctx, issue, 1); err != nil {
return fmt.Errorf("failed to create snapshot: %w", err)
}
summary, err := c.haiku.SummarizeTier1(ctx, issue)
if err != nil {
return fmt.Errorf("failed to summarize with Haiku: %w", err)

View File

@@ -224,18 +224,6 @@ func TestCompactTier1_WithAPI(t *testing.T) {
if afterIssue.AcceptanceCriteria != "" {
t.Error("acceptance criteria should be cleared")
}
snapshots, err := store.GetSnapshots(ctx, issue.ID)
if err != nil {
t.Fatalf("failed to get snapshots: %v", err)
}
if len(snapshots) == 0 {
t.Fatal("snapshot should exist")
}
snapshot := snapshots[0]
if snapshot.Description != issue.Description {
t.Error("snapshot should preserve original description")
}
}
func TestCompactTier1Batch_DryRun(t *testing.T) {

View File

@@ -3,11 +3,8 @@ package sqlite
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/steveyegge/beads/internal/types"
)
// CompactionCandidate represents an issue eligible for compaction
@@ -19,18 +16,6 @@ type CompactionCandidate struct {
DependentCount int
}
// Snapshot represents a saved version of issue content before compaction
type Snapshot struct {
IssueID string `json:"issue_id"`
CompactionLevel int `json:"compaction_level"`
Description string `json:"description"`
Design string `json:"design"`
Notes string `json:"notes"`
AcceptanceCriteria string `json:"acceptance_criteria"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
}
// GetTier1Candidates returns issues eligible for Tier 1 compaction.
// Criteria:
// - Status = closed
@@ -291,166 +276,6 @@ func (s *SQLiteStorage) CheckEligibility(ctx context.Context, issueID string, ti
return false, fmt.Sprintf("invalid tier: %d", tier), nil
}
// CreateSnapshot creates a snapshot of the issue's content before compaction.
// The snapshot includes all text fields and is stored as JSON.
// Multiple snapshots can exist per issue (one per compaction level).
// NOTE: This should be called within the same transaction as the compaction operation.
func (s *SQLiteStorage) CreateSnapshot(ctx context.Context, issue *types.Issue, level int) error {
if level <= 0 {
return fmt.Errorf("invalid compaction level %d; must be >= 1", level)
}
snapshot := Snapshot{
IssueID: issue.ID,
CompactionLevel: level,
Description: issue.Description,
Design: issue.Design,
Notes: issue.Notes,
AcceptanceCriteria: issue.AcceptanceCriteria,
Title: issue.Title,
CreatedAt: time.Now().UTC(),
}
snapshotJSON, err := json.Marshal(snapshot)
if err != nil {
return fmt.Errorf("failed to marshal snapshot: %w", err)
}
query := `
INSERT INTO compaction_snapshots (issue_id, compaction_level, snapshot_json, created_at)
VALUES (?, ?, ?, ?)
`
_, err = s.db.ExecContext(ctx, query, issue.ID, level, snapshotJSON, snapshot.CreatedAt)
if err != nil {
return fmt.Errorf("failed to insert snapshot: %w", err)
}
return nil
}
// RestoreFromSnapshot restores an issue's content from a snapshot.
// Returns the exact original content from the snapshot at the specified level.
// Uses a transaction with optimistic concurrency control to prevent race conditions.
func (s *SQLiteStorage) RestoreFromSnapshot(ctx context.Context, issueID string, level int) error {
if level <= 0 {
return fmt.Errorf("invalid level %d; must be >= 1", level)
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()
var snapshotJSON []byte
err = tx.QueryRowContext(ctx, `
SELECT snapshot_json
FROM compaction_snapshots
WHERE issue_id = ? AND compaction_level = ?
ORDER BY created_at DESC
LIMIT 1
`, issueID, level).Scan(&snapshotJSON)
if err == sql.ErrNoRows {
return fmt.Errorf("no snapshot found for issue %s at level %d", issueID, level)
}
if err != nil {
return fmt.Errorf("failed to query snapshot: %w", err)
}
var snapshot Snapshot
if err = json.Unmarshal(snapshotJSON, &snapshot); err != nil {
return fmt.Errorf("failed to unmarshal snapshot: %w", err)
}
if snapshot.IssueID != issueID {
return fmt.Errorf("snapshot issue_id mismatch: got %s, want %s", snapshot.IssueID, issueID)
}
restoreLevel := snapshot.CompactionLevel - 1
if restoreLevel < 0 {
return fmt.Errorf("invalid restore level computed: %d", restoreLevel)
}
res, err := tx.ExecContext(ctx, `
UPDATE issues
SET description = ?,
design = ?,
notes = ?,
acceptance_criteria = ?,
title = ?,
compaction_level = ?,
updated_at = ?
WHERE id = ? AND COALESCE(compaction_level, 0) = ?
`,
snapshot.Description,
snapshot.Design,
snapshot.Notes,
snapshot.AcceptanceCriteria,
snapshot.Title,
restoreLevel,
time.Now().UTC(),
issueID,
snapshot.CompactionLevel,
)
if err != nil {
return fmt.Errorf("failed to restore issue content: %w", err)
}
rows, _ := res.RowsAffected()
if rows == 0 {
return fmt.Errorf("restore conflict: current compaction_level changed or issue not found")
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("commit restore tx: %w", err)
}
return nil
}
// GetSnapshots returns all snapshots for an issue, ordered by compaction level and creation time.
// Returns the latest snapshot for each level.
func (s *SQLiteStorage) GetSnapshots(ctx context.Context, issueID string) ([]*Snapshot, error) {
query := `
SELECT snapshot_json
FROM compaction_snapshots
WHERE issue_id = ?
ORDER BY compaction_level ASC, created_at DESC
`
rows, err := s.db.QueryContext(ctx, query, issueID)
if err != nil {
return nil, fmt.Errorf("failed to query snapshots: %w", err)
}
defer rows.Close()
var snapshots []*Snapshot
for rows.Next() {
var snapshotJSON []byte
if err := rows.Scan(&snapshotJSON); err != nil {
return nil, fmt.Errorf("failed to scan snapshot: %w", err)
}
var snapshot Snapshot
if err := json.Unmarshal(snapshotJSON, &snapshot); err != nil {
return nil, fmt.Errorf("failed to unmarshal snapshot: %w", err)
}
snapshots = append(snapshots, &snapshot)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return snapshots, nil
}
// ApplyCompaction updates the compaction metadata for an issue after successfully compacting it.
// This sets compaction_level, compacted_at, and original_size fields.
func (s *SQLiteStorage) ApplyCompaction(ctx context.Context, issueID string, level int, originalSize int) error {

View File

@@ -110,69 +110,7 @@ func BenchmarkCheckEligibility(b *testing.B) {
}
}
func BenchmarkCreateSnapshot(b *testing.B) {
store, cleanup := setupBenchDB(b)
defer cleanup()
ctx := context.Background()
issue := &types.Issue{
ID: "bd-1",
Title: "Test Issue",
Description: "Original description with substantial content",
Design: "Design notes with additional context",
Notes: "Additional notes for the issue",
AcceptanceCriteria: "Must meet all requirements",
Status: "closed",
Priority: 2,
IssueType: "task",
ClosedAt: timePtr(time.Now()),
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
b.Fatalf("Failed to create issue: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := store.CreateSnapshot(ctx, issue, i%5+1); err != nil {
b.Fatalf("CreateSnapshot failed: %v", err)
}
}
}
func BenchmarkGetSnapshots(b *testing.B) {
store, cleanup := setupBenchDB(b)
defer cleanup()
ctx := context.Background()
issue := &types.Issue{
ID: "bd-1",
Title: "Test",
Description: "Test description",
Status: "closed",
Priority: 2,
IssueType: "task",
ClosedAt: timePtr(time.Now()),
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
b.Fatalf("Failed to create issue: %v", err)
}
for i := 1; i <= 5; i++ {
if err := store.CreateSnapshot(ctx, issue, i); err != nil {
b.Fatalf("CreateSnapshot failed: %v", err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := store.GetSnapshots(ctx, issue.ID)
if err != nil {
b.Fatalf("GetSnapshots failed: %v", err)
}
}
}
func generateID(b testing.TB, prefix string, n int) string {
func generateID(b testing.TB, prefix string, n int) string{
b.Helper()
return prefix + string(rune('0'+n/10)) + string(rune('0'+n%10))
}

View File

@@ -3,7 +3,6 @@ package sqlite
import (
"context"
"database/sql"
"strings"
"testing"
"time"
@@ -315,236 +314,6 @@ func TestTier1NoCircularDeps(t *testing.T) {
}
}
func TestCreateSnapshot(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
issue := &types.Issue{
ID: "bd-1",
Title: "Test Issue",
Description: "Original description",
Design: "Design notes",
Notes: "Additional notes",
AcceptanceCriteria: "Must work",
Status: "closed",
Priority: 2,
IssueType: "task",
ClosedAt: timePtr(time.Now()),
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
err := store.CreateSnapshot(ctx, issue, 1)
if err != nil {
t.Fatalf("CreateSnapshot failed: %v", err)
}
snapshots, err := store.GetSnapshots(ctx, issue.ID)
if err != nil {
t.Fatalf("GetSnapshots failed: %v", err)
}
if len(snapshots) != 1 {
t.Fatalf("Expected 1 snapshot, got %d", len(snapshots))
}
snapshot := snapshots[0]
if snapshot.Description != issue.Description {
t.Errorf("Expected description %q, got %q", issue.Description, snapshot.Description)
}
if snapshot.Design != issue.Design {
t.Errorf("Expected design %q, got %q", issue.Design, snapshot.Design)
}
if snapshot.Notes != issue.Notes {
t.Errorf("Expected notes %q, got %q", issue.Notes, snapshot.Notes)
}
if snapshot.AcceptanceCriteria != issue.AcceptanceCriteria {
t.Errorf("Expected criteria %q, got %q", issue.AcceptanceCriteria, snapshot.AcceptanceCriteria)
}
}
func TestCreateSnapshotUTF8(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
issue := &types.Issue{
ID: "bd-1",
Title: "UTF-8 Test 🎉",
Description: "Café, résumé, 日本語, emoji 🚀",
Design: "Design with 中文 and émojis 🔥",
Notes: "Notes: ñ, ü, é, à",
AcceptanceCriteria: "Must handle UTF-8 correctly ✅",
Status: "closed",
Priority: 2,
IssueType: "task",
ClosedAt: timePtr(time.Now()),
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
err := store.CreateSnapshot(ctx, issue, 1)
if err != nil {
t.Fatalf("CreateSnapshot failed: %v", err)
}
snapshots, err := store.GetSnapshots(ctx, issue.ID)
if err != nil {
t.Fatalf("GetSnapshots failed: %v", err)
}
if len(snapshots) != 1 {
t.Fatalf("Expected 1 snapshot, got %d", len(snapshots))
}
snapshot := snapshots[0]
if snapshot.Title != issue.Title {
t.Errorf("UTF-8 title not preserved: expected %q, got %q", issue.Title, snapshot.Title)
}
if snapshot.Description != issue.Description {
t.Errorf("UTF-8 description not preserved: expected %q, got %q", issue.Description, snapshot.Description)
}
if snapshot.Design != issue.Design {
t.Errorf("UTF-8 design not preserved: expected %q, got %q", issue.Design, snapshot.Design)
}
}
func TestCreateMultipleSnapshots(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
issue := &types.Issue{
ID: "bd-1",
Title: "Test Issue",
Description: "Original",
Status: "closed",
Priority: 2,
IssueType: "task",
ClosedAt: timePtr(time.Now()),
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
if err := store.CreateSnapshot(ctx, issue, 1); err != nil {
t.Fatalf("CreateSnapshot level 1 failed: %v", err)
}
issue.Description = "Compacted once"
if err := store.CreateSnapshot(ctx, issue, 2); err != nil {
t.Fatalf("CreateSnapshot level 2 failed: %v", err)
}
snapshots, err := store.GetSnapshots(ctx, issue.ID)
if err != nil {
t.Fatalf("GetSnapshots failed: %v", err)
}
if len(snapshots) != 2 {
t.Fatalf("Expected 2 snapshots, got %d", len(snapshots))
}
if snapshots[0].CompactionLevel != 1 {
t.Errorf("Expected first snapshot level 1, got %d", snapshots[0].CompactionLevel)
}
if snapshots[1].CompactionLevel != 2 {
t.Errorf("Expected second snapshot level 2, got %d", snapshots[1].CompactionLevel)
}
}
func TestRestoreFromSnapshot(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
issue := &types.Issue{
ID: "bd-1",
Title: "Original Title",
Description: "Original description",
Design: "Original design",
Notes: "Original notes",
AcceptanceCriteria: "Original criteria",
Status: "closed",
Priority: 2,
IssueType: "task",
ClosedAt: timePtr(time.Now()),
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
if err := store.CreateSnapshot(ctx, issue, 1); err != nil {
t.Fatalf("CreateSnapshot failed: %v", err)
}
_, err := store.db.ExecContext(ctx, `
UPDATE issues
SET description = 'Compacted',
design = '',
notes = '',
acceptance_criteria = '',
compaction_level = 1
WHERE id = ?
`, issue.ID)
if err != nil {
t.Fatalf("Failed to update issue: %v", err)
}
err = store.RestoreFromSnapshot(ctx, issue.ID, 1)
if err != nil {
t.Fatalf("RestoreFromSnapshot failed: %v", err)
}
restored, err := store.GetIssue(ctx, issue.ID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if restored.Description != issue.Description {
t.Errorf("Description not restored: expected %q, got %q", issue.Description, restored.Description)
}
if restored.Design != issue.Design {
t.Errorf("Design not restored: expected %q, got %q", issue.Design, restored.Design)
}
if restored.Notes != issue.Notes {
t.Errorf("Notes not restored: expected %q, got %q", issue.Notes, restored.Notes)
}
if restored.AcceptanceCriteria != issue.AcceptanceCriteria {
t.Errorf("Criteria not restored: expected %q, got %q", issue.AcceptanceCriteria, restored.AcceptanceCriteria)
}
}
func TestRestoreSnapshotNoSnapshot(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
issue := &types.Issue{
ID: "bd-1",
Title: "Test",
Description: "Test",
Status: "closed",
Priority: 2,
IssueType: "task",
ClosedAt: timePtr(time.Now()),
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
err := store.RestoreFromSnapshot(ctx, issue.ID, 1)
if err == nil {
t.Fatal("Expected error when no snapshot exists")
}
if !strings.Contains(err.Error(), "no snapshot found") {
t.Errorf("Expected 'no snapshot found' error, got: %v", err)
}
}
func TestApplyCompaction(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()