Files
beads/internal/rpc/server_compact.go
Steve Yegge bbb1725c32 Replace getStorageForRequest with s.storage (bd-32)
- Replaced all getStorageForRequest(req) calls with direct s.storage access
- Updated 5 handler files: server_issues_epics.go (~8 calls), server_labels_deps_comments.go (~4 calls), server_compact.go (~2 calls), server_export_import_auto.go (~2 calls), server_routing_validation_diagnostics.go (~1 call)
- Only remaining references are in server_cache_storage.go (to be deleted in bd-33) and server_eviction_test.go (to be deleted in bd-34)
- Part of bd-29 epic to remove daemon storage cache

Amp-Thread-ID: https://ampcode.com/threads/T-239a5531-68a5-4c98-b85d-0e3512b2553c
Co-authored-by: Amp <amp@ampcode.com>
2025-10-27 23:14:38 -07:00

276 lines
6.1 KiB
Go

package rpc
import (
"encoding/json"
"fmt"
"time"
"github.com/steveyegge/beads/internal/compact"
"github.com/steveyegge/beads/internal/storage/sqlite"
)
func (s *Server) handleCompact(req *Request) Response {
var args CompactArgs
if err := json.Unmarshal(req.Args, &args); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid compact args: %v", err),
}
}
store := s.storage
sqliteStore, ok := store.(*sqlite.SQLiteStorage)
if !ok {
return Response{
Success: false,
Error: "compact requires SQLite storage",
}
}
config := &compact.Config{
APIKey: args.APIKey,
Concurrency: args.Workers,
DryRun: args.DryRun,
}
if config.Concurrency <= 0 {
config.Concurrency = 5
}
compactor, err := compact.New(sqliteStore, args.APIKey, config)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to create compactor: %v", err),
}
}
ctx := s.reqCtx(req)
startTime := time.Now()
if args.IssueID != "" {
if !args.Force {
eligible, reason, err := sqliteStore.CheckEligibility(ctx, args.IssueID, args.Tier)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to check eligibility: %v", err),
}
}
if !eligible {
return Response{
Success: false,
Error: fmt.Sprintf("%s is not eligible for Tier %d compaction: %s", args.IssueID, args.Tier, reason),
}
}
}
issue, err := sqliteStore.GetIssue(ctx, args.IssueID)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get issue: %v", err),
}
}
originalSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
if args.DryRun {
result := CompactResponse{
Success: true,
IssueID: args.IssueID,
OriginalSize: originalSize,
Reduction: "70-80%",
DryRun: true,
}
data, _ := json.Marshal(result)
return Response{
Success: true,
Data: data,
}
}
if args.Tier == 1 {
err = compactor.CompactTier1(ctx, args.IssueID)
} else {
return Response{
Success: false,
Error: "Tier 2 compaction not yet implemented",
}
}
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("compaction failed: %v", err),
}
}
issueAfter, _ := sqliteStore.GetIssue(ctx, args.IssueID)
compactedSize := 0
if issueAfter != nil {
compactedSize = len(issueAfter.Description)
}
duration := time.Since(startTime)
result := CompactResponse{
Success: true,
IssueID: args.IssueID,
OriginalSize: originalSize,
CompactedSize: compactedSize,
Reduction: fmt.Sprintf("%.1f%%", float64(originalSize-compactedSize)/float64(originalSize)*100),
Duration: duration.String(),
}
data, _ := json.Marshal(result)
return Response{
Success: true,
Data: data,
}
}
if args.All {
var candidates []*sqlite.CompactionCandidate
switch args.Tier {
case 1:
tier1, err := sqliteStore.GetTier1Candidates(ctx)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get Tier 1 candidates: %v", err),
}
}
candidates = tier1
case 2:
tier2, err := sqliteStore.GetTier2Candidates(ctx)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get Tier 2 candidates: %v", err),
}
}
candidates = tier2
default:
return Response{
Success: false,
Error: fmt.Sprintf("invalid tier: %d (must be 1 or 2)", args.Tier),
}
}
if len(candidates) == 0 {
result := CompactResponse{
Success: true,
Results: []CompactResult{},
}
data, _ := json.Marshal(result)
return Response{
Success: true,
Data: data,
}
}
issueIDs := make([]string, len(candidates))
for i, c := range candidates {
issueIDs[i] = c.IssueID
}
batchResults, err := compactor.CompactTier1Batch(ctx, issueIDs)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("batch compaction failed: %v", err),
}
}
results := make([]CompactResult, 0, len(batchResults))
for _, r := range batchResults {
result := CompactResult{
IssueID: r.IssueID,
Success: r.Err == nil,
OriginalSize: r.OriginalSize,
CompactedSize: r.CompactedSize,
}
if r.Err != nil {
result.Error = r.Err.Error()
} else if r.OriginalSize > 0 && r.CompactedSize > 0 {
result.Reduction = fmt.Sprintf("%.1f%%", float64(r.OriginalSize-r.CompactedSize)/float64(r.OriginalSize)*100)
}
results = append(results, result)
}
duration := time.Since(startTime)
response := CompactResponse{
Success: true,
Results: results,
Duration: duration.String(),
DryRun: args.DryRun,
}
data, _ := json.Marshal(response)
return Response{
Success: true,
Data: data,
}
}
return Response{
Success: false,
Error: "must specify --all or --id",
}
}
func (s *Server) handleCompactStats(req *Request) Response {
var args CompactStatsArgs
if err := json.Unmarshal(req.Args, &args); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid compact stats args: %v", err),
}
}
store := s.storage
sqliteStore, ok := store.(*sqlite.SQLiteStorage)
if !ok {
return Response{
Success: false,
Error: "compact stats requires SQLite storage",
}
}
ctx := s.reqCtx(req)
tier1, err := sqliteStore.GetTier1Candidates(ctx)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get Tier 1 candidates: %v", err),
}
}
tier2, err := sqliteStore.GetTier2Candidates(ctx)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get Tier 2 candidates: %v", err),
}
}
stats := CompactStatsData{
Tier1Candidates: len(tier1),
Tier2Candidates: len(tier2),
Tier1MinAge: "30 days",
Tier2MinAge: "90 days",
TotalClosed: 0, // Could query for this but not critical
}
result := CompactResponse{
Success: true,
Stats: &stats,
}
data, _ := json.Marshal(result)
return Response{
Success: true,
Data: data,
}
}