fix: defer SQLite metadata updates until after git commit (GH#885)
This fixes the atomicity gap where exportToJSONL would update SQLite metadata (clear dirty flags, update content hash, last_import_time) BEFORE the git commit. If git commit failed, SQLite would incorrectly indicate the sync succeeded. Changes: - Add ExportResult struct to capture export metadata for deferred finalization - Add exportToJSONLDeferred() that exports without updating metadata - Add finalizeExport() to update metadata after git commit succeeds - Update daemon_sync.go sync flows to defer metadata updates Now the sync flow is truly atomic: metadata only updates after git commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Executed-By: beads/crew/dave Rig: beads Role: crew
This commit is contained in:
committed by
Steve Yegge
parent
68f5bb24f8
commit
ede940b5d2
@@ -437,25 +437,29 @@ func performExport(ctx context.Context, store storage.Storage, autoCommit, autoP
|
||||
}
|
||||
log.log("Exported to JSONL")
|
||||
|
||||
// Update export metadata for multi-repo support with stable keys
|
||||
multiRepoPaths := getMultiRepoJSONLPaths()
|
||||
if multiRepoPaths != nil {
|
||||
// Multi-repo mode: update metadata for each JSONL with stable repo key
|
||||
for _, path := range multiRepoPaths {
|
||||
repoKey := getRepoKeyForPath(path)
|
||||
updateExportMetadata(exportCtx, store, path, log, repoKey)
|
||||
// GH#885: Defer metadata updates until AFTER git commit succeeds.
|
||||
// This is a helper to finalize the export after git operations.
|
||||
finalizeExportMetadata := func() {
|
||||
// Update export metadata for multi-repo support with stable keys
|
||||
multiRepoPaths := getMultiRepoJSONLPaths()
|
||||
if multiRepoPaths != nil {
|
||||
// Multi-repo mode: update metadata for each JSONL with stable repo key
|
||||
for _, path := range multiRepoPaths {
|
||||
repoKey := getRepoKeyForPath(path)
|
||||
updateExportMetadata(exportCtx, store, path, log, repoKey)
|
||||
}
|
||||
} else {
|
||||
// Single-repo mode: update metadata for main JSONL
|
||||
updateExportMetadata(exportCtx, store, jsonlPath, log, "")
|
||||
}
|
||||
} else {
|
||||
// Single-repo mode: update metadata for main JSONL
|
||||
updateExportMetadata(exportCtx, store, jsonlPath, log, "")
|
||||
}
|
||||
|
||||
// Update database mtime to be >= JSONL mtime (fixes #278, #301, #321)
|
||||
// This prevents validatePreExport from incorrectly blocking on next export
|
||||
// with "JSONL is newer than database" after daemon auto-export
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
log.log("Warning: failed to update database mtime: %v", err)
|
||||
// Update database mtime to be >= JSONL mtime (fixes #278, #301, #321)
|
||||
// This prevents validatePreExport from incorrectly blocking on next export
|
||||
// with "JSONL is newer than database" after daemon auto-export
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
log.log("Warning: failed to update database mtime: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-commit if enabled (skip in git-free mode)
|
||||
@@ -497,6 +501,12 @@ func performExport(ctx context.Context, store storage.Storage, autoCommit, autoP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GH#885: NOW finalize metadata after git commit succeeded
|
||||
finalizeExportMetadata()
|
||||
} else if skipGit {
|
||||
// Git-free mode: finalize immediately since there's no git to wait for
|
||||
finalizeExportMetadata()
|
||||
}
|
||||
|
||||
if skipGit {
|
||||
@@ -720,28 +730,34 @@ func performSync(ctx context.Context, store storage.Storage, autoCommit, autoPus
|
||||
}
|
||||
log.log("Exported to JSONL")
|
||||
|
||||
// Update export metadata for multi-repo support with stable keys
|
||||
if multiRepoPaths != nil {
|
||||
// Multi-repo mode: update metadata for each JSONL with stable repo key
|
||||
for _, path := range multiRepoPaths {
|
||||
repoKey := getRepoKeyForPath(path)
|
||||
updateExportMetadata(syncCtx, store, path, log, repoKey)
|
||||
}
|
||||
} else {
|
||||
// Single-repo mode: update metadata for main JSONL
|
||||
updateExportMetadata(syncCtx, store, jsonlPath, log, "")
|
||||
}
|
||||
|
||||
// Update database mtime to be >= JSONL mtime
|
||||
// This prevents validatePreExport from incorrectly blocking on next export
|
||||
// GH#885: Defer metadata updates until AFTER git commit succeeds.
|
||||
// Define helper to finalize after git operations.
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
log.log("Warning: failed to update database mtime: %v", err)
|
||||
finalizeExportMetadata := func() {
|
||||
// Update export metadata for multi-repo support with stable keys
|
||||
if multiRepoPaths != nil {
|
||||
// Multi-repo mode: update metadata for each JSONL with stable repo key
|
||||
for _, path := range multiRepoPaths {
|
||||
repoKey := getRepoKeyForPath(path)
|
||||
updateExportMetadata(syncCtx, store, path, log, repoKey)
|
||||
}
|
||||
} else {
|
||||
// Single-repo mode: update metadata for main JSONL
|
||||
updateExportMetadata(syncCtx, store, jsonlPath, log, "")
|
||||
}
|
||||
|
||||
// Update database mtime to be >= JSONL mtime
|
||||
// This prevents validatePreExport from incorrectly blocking on next export
|
||||
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
||||
log.log("Warning: failed to update database mtime: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Skip git operations, snapshot capture, deletion tracking, and import in local-only mode
|
||||
// Local-only sync is export-only since there's no remote to sync with
|
||||
if skipGit {
|
||||
// Git-free mode: finalize immediately since there's no git to wait for
|
||||
finalizeExportMetadata()
|
||||
log.log("Local %s complete", mode)
|
||||
return
|
||||
}
|
||||
@@ -793,6 +809,9 @@ func performSync(ctx context.Context, store storage.Storage, autoCommit, autoPus
|
||||
log.log("Committed changes")
|
||||
}
|
||||
}
|
||||
|
||||
// GH#885: NOW finalize metadata after git commit succeeded
|
||||
finalizeExportMetadata()
|
||||
}
|
||||
|
||||
// Pull (try sync branch first)
|
||||
|
||||
Reference in New Issue
Block a user