fix(import): preserve comment created_at timestamps during import (#735)

Comment timestamps were being overwritten with CURRENT_TIMESTAMP during
import, causing infinite sync loops between hosts as each import would
update timestamps.

Added ImportIssueComment() method that accepts and preserves the original
timestamp from JSONL, and updated importComments() to use it.

Closes #735
This commit is contained in:
Steve Yegge
2025-12-24 12:45:20 -08:00
parent 7f8f8f69d1
commit 9743b45c6d
2 changed files with 53 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ import (
"os"
"sort"
"strings"
"time"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/linear"
@@ -892,7 +893,10 @@ func importComments(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issu
for _, comment := range issue.Comments {
key := fmt.Sprintf("%s:%s", comment.Author, strings.TrimSpace(comment.Text))
if !existingComments[key] {
if _, err := sqliteStore.AddIssueComment(ctx, issue.ID, comment.Author, comment.Text); err != nil {
// Use ImportIssueComment to preserve original timestamp (GH#735)
// Format timestamp as RFC3339 for SQLite compatibility
createdAt := comment.CreatedAt.UTC().Format(time.RFC3339)
if _, err := sqliteStore.ImportIssueComment(ctx, issue.ID, comment.Author, comment.Text, createdAt); err != nil {
if opts.Strict {
return fmt.Errorf("error adding comment to %s: %w", issue.ID, err)
}

View File

@@ -52,6 +52,54 @@ func (s *SQLiteStorage) AddIssueComment(ctx context.Context, issueID, author, te
return comment, nil
}
// ImportIssueComment adds a comment during import, preserving the original timestamp.
// Unlike AddIssueComment which uses CURRENT_TIMESTAMP, this method uses the provided
// createdAt time from the JSONL file. This prevents timestamp drift during sync cycles.
// GH#735: Comment created_at timestamps were being overwritten with current time during import.
func (s *SQLiteStorage) ImportIssueComment(ctx context.Context, issueID, author, text string, createdAt string) (*types.Comment, error) {
// Verify issue exists
var exists bool
err := s.db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM issues WHERE id = ?)`, issueID).Scan(&exists)
if err != nil {
return nil, fmt.Errorf("failed to check issue existence: %w", err)
}
if !exists {
return nil, fmt.Errorf("issue %s not found", issueID)
}
// Insert comment with provided timestamp
result, err := s.db.ExecContext(ctx, `
INSERT INTO comments (issue_id, author, text, created_at)
VALUES (?, ?, ?, ?)
`, issueID, author, text, createdAt)
if err != nil {
return nil, fmt.Errorf("failed to insert comment: %w", err)
}
// Get the inserted comment ID
commentID, err := result.LastInsertId()
if err != nil {
return nil, fmt.Errorf("failed to get comment ID: %w", err)
}
// Fetch the complete comment
comment := &types.Comment{}
err = s.db.QueryRowContext(ctx, `
SELECT id, issue_id, author, text, created_at
FROM comments WHERE id = ?
`, commentID).Scan(&comment.ID, &comment.IssueID, &comment.Author, &comment.Text, &comment.CreatedAt)
if err != nil {
return nil, fmt.Errorf("failed to fetch comment: %w", err)
}
// Mark issue as dirty for JSONL export
if err := s.MarkIssueDirty(ctx, issueID); err != nil {
return nil, fmt.Errorf("failed to mark issue dirty: %w", err)
}
return comment, nil
}
// GetIssueComments retrieves all comments for an issue
func (s *SQLiteStorage) GetIssueComments(ctx context.Context, issueID string) ([]*types.Comment, error) {
rows, err := s.db.QueryContext(ctx, `