Your file watcher just synced its own write. Again. synckit, the Go substrate under reposync and cookiesync, ships anti-echo watching, unix-socket RPC, a host mesh, and flock-backed state, each written once.
go get github.com/yasyf/synckitA persistent RPC suite with exact build admission, same-UID trust, multiplexing,
and bounded 16 MiB payloads is already wired. Register product methods, then pass
the dispatcher, daemonkit program, and product preparation callback to
helperruntime.New:
package main
import (
"context"
"github.com/yasyf/synckit/rpc"
)
func dispatcher() *rpc.Dispatcher {
d := rpc.NewDispatcher()
d.Register("ping", func(ctx context.Context, p map[string]any) (any, error) {
return map[string]any{"pong": p["msg"]}, nil
})
return d
}Driving with an agent? Paste this:
Run `go get github.com/yasyf/synckit`, then use its rpc package to stand up a
unix-socket server: register handlers on `rpc.NewDispatcher` and compose the
dispatcher with `helperruntime.New`, whose `helperruntime.Spec` derives the
socket from the helper's launchd label. Call it from another process with
`syncservice.Resident(name)`, or open that same spec yourself —
`daemonkit.Open(spec)`, then `rpc.NewClient(rpc.ClientConfig{Open: ...})` over
the client's `Business` lane. Listener ownership, publication admission,
readiness, trust, and bounded framing stay in daemonkit.
Every "keep X in sync across my machines" tool re-implements the same daemon: a socket server, host discovery, launchd plists, a reconcile tick, a watch supervisor. Ship a manifest instead:
brew install yasyf/tap/synckitd
synckitd register manifest.jsonsynckitd installs the manifest under ~/.config/synckit/manifests and drives your tool's typed sync service — list, reconcile, sync — through either a resident Unix socket or a socketpair-confined local spawned session. Remote calls use Synckit's fixed rpc-serve-v1 command over strict host-key-pinned SSH. The daemon never imports your code.
Process-backed transports are private to synckitd and run within a daemonkit
Ctx or Owned scope. Resident socket helpers use helperruntime.New with their
daemonkit Program, dispatcher, frame limit, and a preparation callback that
receives the Ctx.
Your daemon writes a file, fsnotify fires, the watcher syncs the write it just made, and around it goes. The watch engine breaks the loop:
eng := watch.NewEngine(resolver, notifier, digest, 2*time.Second, peers)
eng.OnEvent(ctx, id)The engine debounces, dedupes on the resolved fingerprint, and records what it applied before notifying peers — so the echo of its own write resolves to the recorded fingerprint and dies there instead of fanning out again.
Push-based sync between two daemons is a feedback loop: each write triggers the other's. converge.Reconcile is pull-only:
results, err := converge.Reconcile(ctx, lock, driver, fetcher, peers, origin)A pass fetches every peer's registry read-only, folds them in with the CRDT merge (a LWW-element-set join), and performs exactly one write: the local SaveRegistry. Merge in any order and every replica lands on the identical registry; an unreachable peer is logged and skipped, never fatal.
| Package | What it holds |
|---|---|
rpc |
Exact persistent daemonkit sessions carrying typed {method,params} calls with same-UID trust and bounded frames |
syncservice |
The typed sync contract over rpc plus the resident socket transport |
watch |
The generic anti-echo watch engine: debounce, fingerprint dedupe, record-before-notify, concurrent peer fan-out, busy gating |
watchbackend |
Filesystem events mapped to watch ids over recursive fsnotify (inotify/kqueue) |
hostregistry |
The host mesh: reachability detection, Tailscale and Bonjour discovery, an ssh runner, flock-guarded state.json |
cregistry |
LWW-element-set CRDT registry with per-item payloads; pure and clock-free |
converge |
The pull-only convergent-reconcile pass over a cregistry registry |
manifest |
The JSON manifest a consumer registers, plus discovery and validation |
daemon |
The synckitd command tree, daemonkit lifecycle runtime, and product-specific typed LaunchAgent policy |
codec |
Config-free JSON codecs, e.g. the canonical Go-duration string |
tui |
Shared bubbletea terminal UI: a tab router plus the built-in Hosts tab |
reposync and cookiesync import this one substrate, so the wire formats, lock semantics, and fan-out constants two daemons must agree on byte-for-byte are defined once and tested once.
synckitd is the one per-machine daemon behind every consumer: it owns the shared host mesh, the RPC socket, the reconcile tick, and the watch supervisor. synckitd status prints the mesh, registered manifests, the label-derived socket paths, and daemon liveness; synckitd --help carries the full command surface.
Status: pre-1.0 — the API still moves between minors. Licensed under PolyForm Noncommercial 1.0.0.
