Skip to content

fix(sync): let the stale-issue backfill settle - #218

Merged
marcus merged 1 commit into
marcus:mainfrom
jubishop:worktree-stale-backfill
Oct 16, 2026
Merged

marcus merged 1 commit into
marcus:mainfrom
jubishop:worktree-stale-backfill

Conversation

@jubishop

Copy link
Copy Markdown
Contributor

Summary

On a self-hosted td-sync with one Mac and 23 linked projects, every td command appended duplicate issue snapshots to action_log and pushed them, from 1 to 45 per command depending on the project. BackfillStaleIssues never settled. It runs on each sync until a project's first pull, and a device that is a project's only writer never pulls an event, because autosync pulls skip the device's own events. So every sync backfilled the same issues again.

The check finds an issue's latest action with ORDER BY timestamp DESC, which compares text, and two things made it match the same issues after backfilling them:

  • The backfilled row is stamped with the issue's updated_at, bound as a time.Time, so the driver writes 2026-09-30 14:20:48.774338-07:00. Logged actions use 2026-09-30T21:20:48.774338Z. For the same instant the backfilled row sorts lower, so a statusless action such as a delete stays the latest and the issue looks stale again.
  • approve logs {"issue": {...}, "created_review_id": ...}. The sync engine unwraps that (unwrapIssuePayload), but statusMatches reads only a top-level status, so every approved issue looked stale.

Changes:

  • The latest-action subqueries order by julianday(timestamp), then rowid. julianday parses both formats and SQLite's CURRENT_TIMESTAMP, which are what action_log holds after migration 36 rewrote Go String() values. Same-instant ties go to the row logged last.
  • A backfilled row is stamped at the later of updated_at (or now, without one) and the latest action, so it becomes the latest action even when a statusless action is stamped after updated_at.
  • statusMatches unwraps the nested issue payload.

The timestamp format that td writes is unchanged.

Testing

  • make test, go vet ./..., and gofmt pass. golangci-lint v2.13.1 with GOOS=linux and Go 1.27.0, as in CI, reports 0 issues.
  • go test -race -count=1 ./internal/sync/ ./test/syncharness/... passes.
  • New tests:
    • Fail on main: the backfill settles after one run for a deleted issue, when a statusless action is stamped after updated_at, and when the issue has no updated_at; actions in both formats are ordered by time; approve's nested status matches.
    • Guard an alternative I tried: an orphan create backfilled in the same pass, stamped with created_at, does not become the latest action. Ordering by rowid fails this.
  • Manual: real td binaries against a local v0.67.0 td-sync. I linked copies of all 23 of my databases, with earlier backfill rows removed so each starts unsynced, then ran td list repeatedly. With v0.67.0, action_log grew on every command in all 23. With this branch, it grew in none, and the first sync added fewer backfill rows.

Not in this PR

  • The backfills still bind time.Time, so their rows keep a local-offset format that differs from other action_log rows. Writing the canonical format would make backfilled create rows sort among real actions in readers such as GetLastAction, where they could tie with the action that triggered them in td undo. That seemed worth its own change, along with normalizing existing rows.
  • A device that is a project's only writer keeps last_pulled_server_seq at 0. With this change that no longer causes duplicates, so I left the cursor alone. Advancing it past the device's own events would skip them in the manual td sync pull, which re-applies them for convergence, and would turn off snapshot bootstrap for a relinked database.

🤖 Generated with Claude Code

- The stale check orders an issue's actions by time instead of text, so
  backfilled rows, which carry a local UTC offset, sort with the rest.
- Backfilled rows are stamped no earlier than the issue's latest action.
- The check reads the status that approve nests under "issue".

Before, every sync before a project's first pull backfilled the same issues
again and pushed duplicate snapshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marcus
marcus merged commit e89ae26 into marcus:main Oct 16, 2026
3 checks passed
@jubishop
jubishop deleted the worktree-stale-backfill branch October 16, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants