From cdc156428c48bce66040e58c8f0c27d5e03ad044 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 27 Nov 2025 00:11:01 -0800 Subject: [PATCH] fix(staleness): use RFC3339Nano precision for last_import_time (#399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The staleness check compares last_import_time against JSONL file mtime. File mtime has nanosecond precision, but last_import_time was stored with only second precision (RFC3339). This caused a race condition where the stored time could be slightly earlier than the file mtime, triggering false "Database out of sync" errors - particularly in git worktrees. Changed all 6 locations that set last_import_time to use RFC3339Nano. The CheckStaleness parser already handles both formats, so this is backward compatible. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cmd/bd/autoflush.go | 3 ++- cmd/bd/daemon_sync.go | 3 ++- cmd/bd/import.go | 3 ++- cmd/bd/sync.go | 3 ++- internal/autoimport/autoimport.go | 6 ++++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/bd/autoflush.go b/cmd/bd/autoflush.go index 03bb27de..b7b459ce 100644 --- a/cmd/bd/autoflush.go +++ b/cmd/bd/autoflush.go @@ -231,7 +231,8 @@ func autoImportIfNewer() { } // Store import timestamp (bd-159: for staleness detection) - importTime := time.Now().Format(time.RFC3339) + // Use RFC3339Nano for nanosecond precision to avoid race with file mtime (fixes #399) + importTime := time.Now().Format(time.RFC3339Nano) if err := store.SetMetadata(ctx, "last_import_time", importTime); err != nil { fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_time after import: %v\n", err) } diff --git a/cmd/bd/daemon_sync.go b/cmd/bd/daemon_sync.go index c53b1e04..8b2fc534 100644 --- a/cmd/bd/daemon_sync.go +++ b/cmd/bd/daemon_sync.go @@ -295,7 +295,8 @@ func updateExportMetadata(ctx context.Context, store storage.Storage, jsonlPath log.log("Next export may require running 'bd import' first") } - exportTime := time.Now().Format(time.RFC3339) + // Use RFC3339Nano for nanosecond precision to avoid race with file mtime (fixes #399) + exportTime := time.Now().Format(time.RFC3339Nano) if err := store.SetMetadata(ctx, timeKey, exportTime); err != nil { log.log("Warning: failed to update %s: %v", timeKey, err) } diff --git a/cmd/bd/import.go b/cmd/bd/import.go index c725f89f..b2dc078a 100644 --- a/cmd/bd/import.go +++ b/cmd/bd/import.go @@ -357,7 +357,8 @@ NOTE: Import requires direct database access and does not work with daemon mode. // is unavailable. This ensures import operations always succeed even if metadata storage fails. debug.Logf("Warning: failed to update last_import_hash: %v", err) } - importTime := time.Now().Format(time.RFC3339) + // Use RFC3339Nano for nanosecond precision to avoid race with file mtime (fixes #399) + importTime := time.Now().Format(time.RFC3339Nano) if err := store.SetMetadata(ctx, "last_import_time", importTime); err != nil { // Non-fatal warning (see above comment about graceful degradation) debug.Logf("Warning: failed to update last_import_time: %v", err) diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index 28b04eef..d0c3a676 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -945,7 +945,8 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error { // is unavailable. This ensures export operations always succeed even if metadata storage fails. fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_hash: %v\n", err) } - exportTime := time.Now().Format(time.RFC3339) + // Use RFC3339Nano for nanosecond precision to avoid race with file mtime (fixes #399) + exportTime := time.Now().Format(time.RFC3339Nano) if err := store.SetMetadata(ctx, "last_import_time", exportTime); err != nil { // Non-fatal warning (see above comment about graceful degradation) fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_time: %v\n", err) diff --git a/internal/autoimport/autoimport.go b/internal/autoimport/autoimport.go index 7cc0f96a..771150e7 100644 --- a/internal/autoimport/autoimport.go +++ b/internal/autoimport/autoimport.go @@ -93,7 +93,8 @@ func AutoImportIfNewer(ctx context.Context, store storage.Storage, dbPath string notify.Debugf("auto-import skipped, JSONL unchanged (hash match)") // Update last_import_time to prevent repeated staleness warnings // This handles the case where mtime changed but content didn't (e.g., git pull, touch) - importTime := time.Now().Format(time.RFC3339) + // Use RFC3339Nano for nanosecond precision to avoid race with file mtime (fixes #399) + importTime := time.Now().Format(time.RFC3339Nano) if err := store.SetMetadata(ctx, "last_import_time", importTime); err != nil { notify.Warnf("failed to update last_import_time: %v", err) } @@ -133,7 +134,8 @@ func AutoImportIfNewer(ctx context.Context, store storage.Storage, dbPath string notify.Warnf("This may cause auto-import to retry the same import on next operation.") } - importTime := time.Now().Format(time.RFC3339) + // Use RFC3339Nano for nanosecond precision to avoid race with file mtime (fixes #399) + importTime := time.Now().Format(time.RFC3339Nano) if err := store.SetMetadata(ctx, "last_import_time", importTime); err != nil { notify.Warnf("failed to update last_import_time after import: %v", err) }