Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-011: Workspace freshness: git remote drift detection

Date: 2026-05-24 (revised 2026-05-25) Status: Accepted (Phase 1 in SYN-93; Phase 2 implemented in SYN-93b, 2026-06-04) Author: Rômulo Febasi


Context

ADR-010 (workspace federation) lets one machine register many workspaces and ADR-010 §F4 captures git_remote automatically on register. Once workspaces start being shared via git between team-mates, the file you read locally can be silently behind the remote, and Synapse, by default, returns confidently wrong data.

For a tool whose value is “the AI consults your world with your facts”, returning stale facts is the worst failure mode: confident, silent, undetectable.

The user proposed: every N minutes, check whether the local workspace is in sync with origin. If behind, warn (in the CLI and through the MCP) and recommend git pull. Crucially, never block the user.

This ADR locks the design so SYN-93 lands opinionated, not improvised.

Decision

Synapse exposes a freshness signal per workspace: an advisory status derived from a periodic comparison between local HEAD and the configured git remote. The signal is advisory, never authoritative and never blocking. It surfaces in two places:

  • CLI: a one-line note on stderr before the regular output of commands that read from a workspace whose freshness is behind or diverged.
  • MCP: every tool response that returns data sourced from a workspace embeds a freshness field; the AI assistant decides whether to surface it to the human.

Design rules

  1. Never blocking. A freshness check that fails (network error, timeout, auth problem) does not fail the command. Status falls back to unreachable; the command runs normally.
  2. Bounded latency. The async dispatcher has a hard 3-second timeout per remote check. Past that, the check is aborted; the stored status keeps its previous value.
  3. Async after the work. A command that reads a workspace triggers a background check after the command has already responded to the user. The current command sees the previous check’s result, which is acceptable because the goal is detection, not blocking.
  4. Rate-limited. A check fires at most once per workspace per freshness.interval_minutes (default 30). Two parallel commands seeing a stale stamp may both fire, the race is benign (last writer wins).
  5. Local-only by default. CLAUDE.md forbids shelling out to git from production code, and the simplest signal (drift since the user’s last git fetch) already covers the most common failure mode. Phase 1 (SYN-93) compares the local refs/heads/<branch> SHA with the refs/remotes/origin/<branch> SHA: both read directly from the filesystem, no subprocess, no network, no auth surface. Phase 2 (SYN-93b, deferred) layers a real gix-protocol ls-refs call on top to detect drift the user hasn’t fetched yet, with the 3s timeout and unreachable fallback. The two phases share the same status enum and the same registry columns.
  6. Network path is opt-out, not opt-in. Default is enabled when a workspace has a git_remote. Disable per-workspace with syn ws freshness off <name> or globally in config: [freshness] enabled = false. The SYNAPSE_NO_FRESHNESS=1 env var disables for the current process only (CI-friendly).
  7. No fetching. Synapse never runs git fetch. Recommendation output suggests it to the user; execution belongs to the user.
  8. Privacy: the check uses whatever git credentials are already available (SSH agent, credential helper). Synapse stores no credential and exposes no remote URL beyond what git_remote already cached.

Registry schema delta (lands in SYN-93)

Three columns added to the workspaces table:

ALTER TABLE workspaces ADD COLUMN last_remote_check  TEXT;
ALTER TABLE workspaces ADD COLUMN remote_status      TEXT;
ALTER TABLE workspaces ADD COLUMN remote_behind      INTEGER;

remote_status is one of:

valuemeaning
unknowncheck never ran, or no git_remote
up_to_datelocal HEAD matches remote HEAD of the tracked branch
behindremote has commits the local doesn’t (count in remote_behind)
aheadlocal has commits not pushed (often fine for solo workflows)
divergedboth have commits the other doesn’t
unreachablelast check timed out, hit auth error, or network failed

MCP payload shape

Every tool that returns workspace-sourced data adds a freshness field:

{
  "result": "...",
  "freshness": {
    "workspace": "client-alfa",
    "status": "behind",
    "behind_count": 5,
    "last_check": "2026-05-24T19:42:00Z",
    "advice": "git -C /Users/.../client-alfa pull"
  }
}

advice is always a concrete command the user can copy; the AI is encouraged to surface it verbatim. The field is omitted entirely when status is up_to_date or unknown: silence is the polite default.

CLI advisory

When a command opens a workspace whose remote_status ∈ {behind, diverged}, the binary writes one line to stderr before the normal output:

syn: workspace 'client-alfa' is 5 commit(s) behind origin (last check 12m ago).
syn: run `git -C /Users/.../client-alfa pull` to refresh.

Suppression: SYNAPSE_NO_FRESHNESS=1, --no-freshness CLI flag, or the [freshness] silent_cli = true config.

unreachable status produces no stderr: too noisy on flaky networks. The status is still visible in syn ws list.

Phasing

  • Phase 1 (SYN-93): filesystem-only. Reads .git/HEAD to find the current branch, then compares refs/heads/<branch> with refs/remotes/origin/<branch>. behind_count is reported as a precise count when the local tip is reachable from the remote tip (or vice versa); when histories have diverged, status is diverged and the count is set to -1 (not computed in Phase 1 to keep the implementation walk-free). No subprocess, no network, no credentials.
  • Phase 2 (SYN-93b, shipped 2026-06-04): freshness::check_remote layers a real ls-refs handshake over Phase 1 via gix (no shelling out). It runs on a worker thread under a hard 3s timeout; any failure folds into unreachable, and the runner then retains the previously known status on the row (only bumping last_remote_check) so a transient offline moment never erases a real “behind”. behind_count is precise when the remote commits have been fetched (a bounded 1000-commit gix::rev_walk), and a count-less Behind when the drift is brand-new (objects not yet fetched). Credentials come from the system (SSH agent / credential helper); Synapse stores none. The CLI advisory + MCP enrichment surface the richer status with no changes: except one new string, fresh-behind-unknown, for the count-less case.

Sub-features deferred to backlog

  • syn ws pull that orchestrates git pull (SYN-209 already covers the broader cross-machine sync question).
  • Auto-fetch (vs. only ls-remote): rejected. Pulling refs without user intent is closer to acting than informing.
  • Cross-VCS support (jj, fossil, hg): out of scope; revisit when someone asks.

Consequences

  • + Solves the silent-staleness failure mode without surprising the user (advisory only, opt-out at three levels).
  • + Adds usefulness to MCP: the AI can frame answers as “this was last refreshed 14m ago; the source may be 5 commits behind”.
  • + Discoverable via syn ws list (the column ships in SYN-93 too).
  • − Background async needs care: the dispatcher must not leak child processes when syn exits. Mitigated by a 3s timeout and a detach-on-exit policy.
  • − Workspaces with auth-locked remotes will sit at unreachable forever for users with no credential. Documented; also produces no noise.

Alternatives considered

  • Synchronous check inside every command. Rejected: adds 100-2000ms latency to every read. Async background gives the same signal with negligible foreground cost.
  • Cron-style daemon (synd) that polls in the background. Rejected: adds a moving part (process lifecycle, restart, logging) for marginal win over the in-process spawn-after-command pattern.
  • Detect via webhook from GitHub. Rejected: only works for GH, requires server setup, leaks workspace identity.
  • git fetch instead of git ls-remote. Rejected: pulls refs the user didn’t ask for; counts as acting on the user’s repo.
  • Always-on, no opt-out. Rejected: corporate networks rate-limit outbound git over time; a CI machine that runs syn in scripts would spam ls-remote.