fix: break sync feedback loop, respect timestamps, surface errors

- Add migration v2: source column on change_log to distinguish local
  vs sync-originated entries, preventing the echo loop where synced
  tasks get re-pushed as local changes
- PushChanges handler now skips save when server version is newer
- Client PushChanges/pushQueuedChanges collect and report marshal errors
  instead of silently dropping them
- De-duplicate getLocalChanges/getLastSyncTime into exported sync
  package functions
- Fix logConflict winner detection via pointer identity instead of
  fragile UUID+timestamp comparison
- Fix sync down to actually parse, save, and tag-sync pulled changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 01:11:04 +01:00
parent 0e3750e755
commit a11f452d3b
5 changed files with 151 additions and 87 deletions
+28
View File
@@ -307,6 +307,13 @@ func runMigrations() error {
END;
`,
},
{
version: 2,
sql: `
ALTER TABLE change_log ADD COLUMN source TEXT NOT NULL DEFAULT 'local';
CREATE INDEX idx_change_log_source ON change_log(source);
`,
},
}
// Apply pending migrations
@@ -409,3 +416,24 @@ func SetChangeLogRetentionDays(days int) error {
_, err := db.Exec("INSERT OR REPLACE INTO sync_config (key, value) VALUES ('change_log_retention_days', ?)", days)
return err
}
// MarkChangeLogAsSync marks the most recent change_log entry for a task UUID
// as originating from sync (not local), preventing the feedback loop where
// synced changes get re-pushed as local changes.
func MarkChangeLogAsSync(taskUUID string) error {
db := GetDB()
if db == nil {
return fmt.Errorf("database not initialized")
}
_, err := db.Exec(`
UPDATE change_log SET source = 'sync'
WHERE id = (
SELECT id FROM change_log
WHERE task_uuid = ?
ORDER BY id DESC
LIMIT 1
)
`, taskUUID)
return err
}