Skip to content
edgesync Cross-Leaf Fossil Sync Over NATS — EdgeSync PR Plan
Login

Cross-Leaf Fossil Sync Over NATS — EdgeSync PR Plan

Date: 2026-05-12 Tracking: EdgeSync #156 Dependency: libfossil CreateOpts.ProjectCode (separate upstream issue, expected v0.6.1)

Context

EdgeSync issue #156 documents two gaps that block cross-process Fossil sharing for sesh:

  1. The hub's NATS sync subject is keyed by per-repo project-code. Each leaf creates a fresh repo → fresh code → siblings subscribe to different subjects → commits don't propagate.
  2. The sync handler is subscribe-only — Repo.Commit never publishes anything, so peers only learn of new commits by explicit polling.

The fix is two coordinated changes, both EdgeSync-side (once the upstream libfossil PR lands):

Goal

A single self-contained EdgeSync PR that, on green CI, makes the sesh integration tests (TestSubLeaf_DoesNotSyncToday, TestHub_DoesNotAccumulateProjectCommitsToday) flippable from "asserts no propagation" to "asserts propagation within N seconds."

In scope

Out of scope (deferred)

Design

1. Config.ProjectCode

type Config struct {
    // ... existing fields ...

    // ProjectCode optionally pins the repo's project-code on first
    // create. When the repo at RepoPath already exists, this value
    // (if non-empty) must match the on-disk project-code or NewHub
    // returns an error — guards against silent topology drift where
    // a caller fixes their config but an old repo is still around.
    // Empty preserves current behavior: generate on create, accept
    // whatever's on disk on open.
    ProjectCode string
}

Threading: openOrCreateRepo(path, bootstrapUser, projectCode). On the create branch, passes through to libfossil.Create(path, CreateOpts{ User: bootstrapUser, ProjectCode: projectCode}). On the open branch, if projectCode != "", read Config("project-code") and compare; error on mismatch.

2. Config.SeedFromUpstream

type Config struct {
    // SeedFromUpstream, when set and RepoPath doesn't yet exist on
    // disk, clones from this URL before opening. The cloned repo
    // carries the upstream's project-code, so children declaring a
    // SeedFromUpstream don't need to also set ProjectCode.
    //
    // URL is an EdgeSync hub HTTP xfer endpoint
    // (e.g. "http://hub.local:8080/"). NATS-native clone is deferred
    // until libfossil HandleSync supports the clone protocol.
    SeedFromUpstream string
}

If both ProjectCode and SeedFromUpstream are set and the clone result's project-code disagrees with ProjectCode, fail.

3. Auto-publish on commit

In hub.Repo.Commit (file: hub/repo.go:129), after the underlying libfossil.Repo commit returns successfully:

This requires hub.Repo to know about the Hub's NATS client. Two shapes:

Lean (a) — keeps the existing Hub.Repo() API surface unchanged and lets direct callers of Repo.Commit benefit from publish without going through Hub. OpenRepo (the standalone path) leaves publish == nil, which the wrapper treats as "no-op."

4. .commit subscriber

New method startCommitSubscriber(cfg Config), invoked from NewHub right after startFossilSyncSubscriber. Subscribes to <prefix>.<project-code>.commit. On message:

The requester is the new plumbing in this PR. Likely a small adapter that implements libfossil.Transport over NATS request/reply, then calling h.repo.handle.Sync(ctx, SyncOpts{...}) against it. See Q2 below — there may be reusable bits in leaf/agent.

5. Integration test

hub/cross_leaf_test.go (new):

Skip via t.Skip if the host can't open two ephemeral ports — same pattern as existing hub tests.

Open questions

File touch list

Tasks (in order)

  1. Blocked: upstream libfossil CreateOpts.ProjectCode lands and v0.6.1 is cut.
  2. Bump go.mod pin; go mod tidy; verify compile.
  3. Config.ProjectCode plumbing through openOrCreateRepo; tests for mismatch on existing repo.
  4. Config.SeedFromUpstream + hub/clone.go; test against an HTTP xfer endpoint (stand up a transient hub in the test).
  5. Repo.publish hook + Commit augmentation; unit test that a commit produces a .commit message with correct payload.
  6. startCommitSubscriber; integration test the receive side (publish a .commit, observe an xfer pull).
  7. NATS Transport adapter (Q2 outcome).
  8. Cross-leaf integration test (the gold-path one).
  9. docs/architecture/sync-protocol.md update.
  10. make / go test ./... / replicate CI locally per global policy.
  11. Push, open PR, wait for human merge.

Sesh side (out of this PR, follow-up by sesh-maintainer)

Once this PR ships and sesh bumps its EdgeSync pin: