Phase 5: sync & freshness — wt sync, opportunistic fetch, staleness#14
Merged
Conversation
A small pure package owns the two decisions the freshness feature shares across commands: whether a recorded fetch has aged past the window, and how to say that age to a human. Time is injected so the display side (ls) and the decision side (new, claim, sync) run the same rule and cannot drift. pool ls's claim-age formatter is folded onto freshness.Age so there is one coarse formatter, not two.
last_fetch is a repo-wide fact, so it is a single RFC3339 file at the state root rather than a per-tree one. A missing or unparseable stamp reads as never-fetched, the same fail-open rule the refresh hash follows: the worst consequence is one extra fetch, always safe.
staleness_hours bounds how old the last base fetch may get before new/claim refetch and ls marks it stale (default applied at the use-site via DefaultStalenessHours, so a loaded config still mirrors its files and a repo can narrow a global window). It round-trips through Save so a read-modify-write like pool resize cannot drop it, and negative values are rejected.
Fetch updates a remote's tracking refs without touching a working tree, so it is safe to run opportunistically. Upstream/UpstreamRemote resolve where a branch tracks, MergeFFOnly fast-forwards a checked-out base (refusing anything but a pure fast-forward), and FetchLocalFF does the same for a base checked out nowhere. All are ff-only: a diverged branch is rejected, never rewritten.
When the base has aged past staleness_hours, wt new and wt claim fetch it once with a one-line notice and, when the local base now trails upstream, point at wt sync. The fetch only updates remote-tracking refs — it never fast-forwards a branch or moves a working tree — so the branch created is exactly what the user expects; --no-fetch opts out, and an offline failure degrades to a warning rather than blocking work. wt ls gains a best-effort staleness note on stderr, silent until wt has a fetch on record so it never touches the network or disturbs the machine listing.
wt sync fetches the base's remote, fast-forwards the local base (reporting and skipping anything that is not a pure fast-forward), and lists how far each tree trails it — never rewriting a branch that carries user commits. --all adds the heavier step of re-parking every idle slot onto the new base under its own lease, gated by the same refresh hash as a claim, so a claimed slot's work is untouched and a racing claim cannot collide. The behind-report runs last, so re-parked slots read as current rather than trailing the base they were just parked on.
Adds a slot-lifecycle diagram and a "staying fresh" section to docs/pool-mode.md, a launchd recipe for scheduled wt sync --all to docs/recipes.md, the staleness_hours reference and last_fetch state entry to docs/configuration.md, wt sync to the README and agents tables, and records Phase 5 in PLAN.md with the two news fragments.
A diverged or busy base printed a notice but returned success, so a scheduled or scripted `wt sync` could not tell a completed sync from a stuck one. fastForwardBase now returns a precondition error (exit 3, D13) for a base it could not advance; the behind-report still prints, and the base is never rewritten.
A failed state-dir write no longer aborts the work the fetch was for: `wt sync` still fast-forwards and reports, and the opportunistic path still prints the behind-upstream pointer. The stamp only costs one extra fetch next time if it is missed (LastFetch fails open), so a write hiccup degrades to a note rather than a hard failure.
A free, detached slot could still hold uncommitted scratch, and the shipped launchd recipe runs `wt sync --all` unattended — so a reset-every-idle-slot policy would silently discard work in progress. Re-park now refuses a dirty slot with a skip notice (tolerating wt's own planted copies), and only announces a slot it actually re-parked.
staleness_hours = 0 is indistinguishable from unset and resolves to the default, not to always-fetch; say so and point at wt sync to force one. Note that the opportunistic fetch targets the base's own configured remote, honoring git config exactly as a manual fetch.
The zero-means-unset default lived in a cli helper one layer above the field it completes. StalenessWindow() co-locates it with StalenessHours and DefaultStalenessHours in config, so every consumer resolves the effective window the same way.
maybeFetchBase spawned a git subprocess (UpstreamRemote) before reading the local fetch stamp, so every wt new and wt claim paid for it even when the base was fetched moments ago — the common case on both hot paths. Evaluate the cheap staleness gate first and only look up the remote when a fetch is actually warranted.
The two consecutive upstream lookups repeated an identical tracks-no-remote message and early return; fold them into one guard. The fetch-stamp record collapses from a warnFetchRecord call in each arm of an if/else into a single chained-error call.
Two reductions in the re-park and fast-forward paths, no behavior change: - Remove the lockless lease.Get pre-check before Acquire. Acquire is the authoritative gate (live and unreadable leases read as held, only provably dead ones are stolen, all under its flock), so the pre-read only duplicated that logic and raced it. - Inline the single-use ffInPlace helper into fastForwardBase. The merge-vs-local-fetch choice is one step of the one fast-forward the function performs, not a separate 5-arg concern.
The local-ref fast-forward path (used by wt sync when the base branch is checked out in no worktree, e.g. the main checkout on a feature branch) had no coverage. Advance an origin, then ff a non-checked-out local main through FetchLocalFF and assert it lands.
Extracting freshness.Age left humanAge as a single-caller pass-through whose doc comment restated freshness.Age's own rationale. Inline it at the pool ls call site so the coarse-age rule lives in exactly one place.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Phase 5 (Sync & freshness): the base stays current without a daemon.
What lands
wt sync [--all]— fetch the base's remote, fast-forward the local base (ff-only; a diverged or busy base is reported and left untouched), and report how far each tree trails it.--allre-parks every idle pool slot onto the new tip under its own lease, gated by the same refresh hash as a claim; claimed slots (and their work) are untouched.wt new/wt claim— a base older thanstaleness_hours(default 24) is fetched once with a one-line notice, pointing atwt syncwhen the local base now trails upstream. The fetch only updates remote-tracking refs — it never fast-forwards a branch or moves a working tree.--no-fetchopts out; an offline failure degrades to a warning.wt lsshows the base fetch age on stderr, best-effort and silent until wt has a fetch on record — it never touches the network.staleness_hoursconfig key,last_fetchstate file, and a newinternal/freshnesspolicy package.Design notes
--allfollows git-town's current-vs-all convention: barewt syncis the fast, safe command; the pool-wide re-park is the deliberate opt-in.clean -ffdbehavior delta and no benchmark yet justifying it. Recorded in PLAN.md.Tests
TDD throughout: unit tests for
freshness,state,config, and thegitxfetch/ff primitives; testscript e2e (sync.txtarwith a local origin fixture advanced by the test — asserts slots re-park and a slot carrying user commits is untouched;fetch.txtarfor the opportunistic fetch,--no-fetch, and the ls note). Full suite green under-race; golangci-lint v2 clean.