Skip to content
edgesync Timeline
Login

Timeline

26 check-ins that include changes to files matching 'hub/*'

2026-08-12
20:50
chore(deps): migrate to go-libfossil v0.9.0 (#195)

* chore(deps): migrate to go-libfossil v0.9.0 (renamed module path)

- import path danmestas/libfossil → danmestas/go-libfossil everywhere - CloneOpts.ProjectCode removed upstream; assert via CloneResult instead - Timeline now takes TimelineOpts (cursor pagination, newest-first) - testutil.HasFossil removed; check FossilBinary() != ""

* ci: retrigger checks

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 38efed7a12 user: [email protected] tags: trunk, main

2026-05-19
18:59
fix(hub): single-owner httpServer construction in NewHub (closes 187) (#188)

TigerStyle: eliminate the shared mutable state instead of guarding it.

Move mux + http.Server construction from ServeHTTP into NewHub. After NewHub returns, h.httpServer and h.httpListener are immutable for the hubs lifetime — single writer (NewHub), many readers (ServeHTTP, Stop). No mutex, no atomic, no sync.Once required: the race is gone by construction.

Add a hub package-private assert helper and positive invariants at entry to ServeHTTP and Stop, surfacing programmer-bugs loudly instead of silently UB-ing on a nil httpServer.

Fixes the 3 data races in TestCrossSessionAutosync and the 3 in TestScope_Project_ConcurrentCommits under -race; hub package suite green at -race -count=3.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 3b6306fdc2 user: [email protected] tags: trunk, main

18:54
fix(hub): single-owner httpServer construction in NewHub (closes 187)

TigerStyle: eliminate the shared mutable state instead of guarding it.

Move mux + http.Server construction from ServeHTTP into NewHub. After NewHub returns, h.httpServer and h.httpListener are immutable for the hubs lifetime — single writer (NewHub), many readers (ServeHTTP, Stop). No mutex, no atomic, no sync.Once required: the race is gone by construction.

Add a hub package-private assert helper and positive invariants at entry to ServeHTTP and Stop, surfacing programmer-bugs loudly instead of silently UB-ing on a nil httpServer.

Fixes the 3 data races in TestCrossSessionAutosync and the 3 in TestScope_Project_ConcurrentCommits under -race; hub package suite green at -race -count=3. leaf check-in: 8a18ec9ebe user: 41898282+github-actions[bot]@users.noreply.github.com tags: trunk, fix/hub-stop-servehttp-race-tigerstyle

16:25
feat(hub): Hub.EnsureUser — idempotent user-table registration (closes #183) (#184)

Add Hub.EnsureUser(login, caps string) error as an idempotent counterpart to Hub.AddUser. Consumers (sesh, others) need "register if absent" semantics; AddUser errors on duplicate, forcing each consumer to write check-then-act + race-recheck wrappers. The user-table layer is the right place to own that idempotency.

Implementation choice (a) — GetUser → CreateUser, serialized by a Repo- level mutex. Picked over (b) "catch CreateUser's already-exists error" because libfossil's CreateUser surfaces a wrapped SQLite UNIQUE-constraint error with no sentinel or typed shape — string-matching would be brittle. A defensive GetUser retry after CreateUser failure absorbs the rare cross- process race where the in-process mutex doesn't apply.

Contract: existing caps are PRESERVED on duplicate (documented). Callers wanting replace-on-conflict use SetUserCaps explicitly.

Tests cover first-time registration, duplicate no-op, caps-change-on- duplicate (preservation), 5-goroutine concurrent race (exactly one row lands), and empty-login rejection. Full hub package + leaf + bridge + CLI suites pass; go vet clean; gofmt clean.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 40c9b9aff3 user: [email protected] tags: trunk, main

2026-05-17
18:47
fix(hub): address pulls to publishing peer to resolve .sync race (#162) (#181)

Three+ hubs sharing a project-code all subscribed to the same broadcast `<prefix>..sync` subject. A `.commit` notification on any one of them caused every other peer to fan out a nats.Request on .sync; every subscribed peer answered, but nats.Request only delivers the first reply. If a stale peer won that race the pull converged against zero blobs and the receiver silently stayed behind.

Two changes close the gap:

1. Each hub now also subscribes to a per-peer addressed sync subject `<prefix>..sync.<peer-id>` (peer-id = the hub's NATS server name). The commit notification carries the publishing hub's peer-id; receivers route their pull-back at that addressed subject so exactly that hub responds. No race regardless of how many other hubs share the project-code.
2. The broadcast `.sync` subscription becomes a NATS queue group (`sync-<project-code>`) so external leaf-agent pushes — which still target the broadcast subject — get exactly one responder per request instead of a fan-out race. Project-code in the queue name scopes the group so unrelated projects sharing a NATS server can't cross-pollinate.

The receiver also drops self-notifications (a publish from this hub round-tripping back via an upstream leaf, which NoEcho doesn't catch across leaf links) and notifications missing a peer-id (older publisher that pre-dates this fix — refusing rather than falling back to a broadcast pull keeps #162 from regressing silently).

`TestCrossLeaf_ThreeHubs_CommitAnnouncePropagatesToAll` is now ungated: it ran red under both -short and full modes before this fix, and passes under both modes (and 10x stability check) after.

Closes #162 Closes #179

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: a96fe486ab user: [email protected] tags: trunk, main

17:44
test(hub): add 3-hub cross-leaf propagation test (#180)

Mirrors the 2-node TestCrossLeaf_SharedProjectCode_PropagatesCommit shape but with a third leaf attached to the same topology root. Asserts that a commit on any peer propagates to BOTH of the other two peers, both directions.

Reproduces the cross-leaf propagation gap reported in #179 and confirms its underlying cause is the multi-responder .sync race filed as #162: with 3+ hubs subscribing to the same .sync subject every peer answers nats.Request, only the first reply wins, and a stale responder silently starves the pull. The first hop (B -> A) is already enough to surface this — A's .sync request can race against C's empty responder even though A and B are the only nodes that need to talk for that direction.

Gated under -short with a skip pointing to #162 so pre-commit and CI stay green. Without -short the test fails at the 10s wait in ~10s, demonstrating the bug. Remove the skip once #162 is fixed; the test then doubles as the regression sentinel for the 3+ hub topology that production sesh relies on.

No production code changes.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 5c36d5498d user: [email protected] tags: trunk, main

2026-05-16
17:55
feat(hub): expose embedded NATS WebSocket endpoint (#177)

Adds Config.EnableWebSocket (opt-in; default off for security since no TLS/WSS is wired up here) plus Config.NATSWebSocketPort (0 = auto-pick, mirroring the sibling NATSClientPort / NATSLeafPort convention). When enabled, the embedded nats-server binds a WebSocket listener on 127.0.0.1:NNNN with NoTLS=true.

Surfaces the resolved listener URL via the new Hub.NATSWebSocketURL() getter, which returns "ws://127.0.0.1:NNNN" when WebSocket is enabled and "" otherwise — same shape as NATSURL() / LeafURL().

Tests cover the disabled-by-default contract, the auto-pick URL shape, and an end-to-end round trip: a nats.go client dials the ws:// URL, subscribes to a subject, and receives a payload published by a TCP client on the same embedded server.

Fixes #176

Co-authored-by: Dan Mestas <[email protected]> check-in: ae52558ba4 user: [email protected] tags: trunk, leaf/v0.0.12, bridge/v0.0.4

2026-05-14
09:32
feat(hub): expose Ready() channel for serve-loop start barrier (#172)

Adds Hub.Ready() returning a channel closed when ServeHTTP has wired up the http.Server and is about to enter the Accept loop. Consumers that advertise hub URLs externally (e.g. sesh writing session state JSON) can wait on Ready() before publishing, closing the window where the TCP listener is bound but the goroutine running ServeHTTP hasn't started processing requests.

Closes #171.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 0f4bfb32df user: [email protected] tags: trunk

2026-05-13
21:03
fix: exclude modernc driver registration from wasm builds (#169)

The init()-based modernc registration added in #166 (v0.0.18) blocked wasm builds because modernc references syscalls unavailable under js/wasm. Adds a //go:build !js && !wasm constraint to hub/driver.go so the registration is skipped on wasm targets — wasm consumers register their own driver (libfossil's ncruces driver supports wasm via its ncruces_js.go file).

Native builds are unchanged: hub still auto-registers modernc. Wasm builds: hub package now compiles successfully under GOOS=js GOARCH=wasm; consumer brings its own driver.

Verified: GOOS=js GOARCH=wasm go vet ./hub/... # clean GOOS=js GOARCH=wasm go build ./hub/... # success go test ./hub/... -timeout 120s # 38/38 pass on native

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 0fa13d595b user: [email protected] tags: trunk, v0.0.19

20:27
refactor(hub): register modernc SQL driver internally (#166)

The hub package promised consumers wouldn't need to import libfossil directly, but they still had to blank-import libfossil's SQL driver to satisfy the underlying db layer's registration requirement. Move the blank import into hub itself (hub/driver.go) so the package fulfills its own dependency.

Consumers like sesh can now reduce their main.go to a plain `import "github.com/danmestas/EdgeSync/hub"` (or whatever wrapper they use) without taking a direct dep on libfossil's internal SQL machinery.

modernc is registered as the production default. If a consumer ever needs ncruces (WASM/WASI), an escape hatch can be added then.

Tests in hub/ drop their now-redundant blank imports.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 754914fd82 user: [email protected] tags: trunk, v0.0.18

2026-05-12
23:50
feat(hub): persist .commit publish watermark across hub restarts (#164)

publishedRID was in-memory only. initPublishWatermark snapshotted the current max ci rid on every start, so any commit that landed in the repo but didn't get its .commit notification out before the hub died (crash mid-publish, HTTP push that completed the SQLite write but crashed before the wrapper ran publishNewCommits, or an offline tool writing to the repo while the hub was down) became silently un-propagated. Peers caught up incidentally on the next commit or not at all. Real gap.

Persist the watermark to the repo's config table under "edgesync:hub-published-rid" after each successful publish. On startup, initPublishWatermark reads the persisted value first and falls back to the max-rid snapshot only when nothing is persisted (cold start). Schedule a one-shot publishNewCommits call publishCatchupDelay (3s) after NewHub so it runs after NATS leaf clients have had time to reconnect on the same leaf port — an immediate catchup fires into a void because nats-core has no per-subscriber queuing.

Stop cancels the pending catchup via catchupStop so the goroutine doesn't outlive the hub.

Regression test TestCrossLeaf_HubRestart_RepublishesUnannouncedCommits: two hubs leaf-linked on a stable leaf port; stop A, open the repo file directly via libfossil and commit (no hub, no NATS, no publish hook), restart A; assert B reads the offline file via the delayed startup catchup. The "open repo file directly" trick is morally equivalent to "hub crashed after libfossil's write but before the wrapper's publish" — both produce a commit that's in the repo but unannounced.

Known limitations (deferred): - If no peer reconnects within publishCatchupDelay, the catchup publish is still lost. JetStream-durable .commit subject would close the gap; tracked as a follow-up. - The .commit subscriber on the pull side doesn't advance the receiving hub's watermark, so a pulled-in commit will be republished on the receiver's next restart. Idempotent on the far side (the subscriber's pull converges to zero blobs) but wasteful chatter; can be tightened in a follow-up. - v1→v2 upgrade with un-announced backlog at the time of upgrade treats those commits as "already known" — same silent-loss behavior as v1; not made worse.

Refs #161 review notes.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 00bc87892b user: [email protected] tags: trunk

23:19
feat(hub): auto-publish on HTTP xfer push (#160) (#161)

`fossil push <hub-http>` from an external CLI agent writes artifacts straight into the SQLite repo via libfossil's xfer protocol, bypassing Repo.Commit and therefore the .commit auto-publish hook installed by NewHub. Peers stay at the prior commit count even though the push succeeded.

Funnel the publish through a new Repo.publishNewCommits helper that enumerates 'ci' events past a per-repo watermark and emits one notification per rid in order. Both Repo.Commit and a thin wrapper around XferHandler in Hub.ServeHTTP call it; the watermark is seeded to the current max ci rid in startCommitSubscriber so SeedFromUpstream clones don't republish their history. publishMu serialises the scan/publish/advance triple so concurrent local-commit and HTTP-push callers cannot republish the same rid.

Regression test TestCrossLeaf_HTTPPush_PropagatesCommit mirrors TestCrossLeaf_SeedFromUpstream_PropagatesCommit: two leaf-linked hubs plus a standalone libfossil.Repo as the external agent that clones from A, commits locally, and pushes back via HTTP. Asserts B reads the new file within 10s.

Closes #160

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 3fa4ea607f user: [email protected] tags: trunk

21:09
feat(cli): expose hub --project-code and --seed-from-upstream flags (#159)

The hub.Config.ProjectCode and hub.Config.SeedFromUpstream fields landed in #157 but were unreachable from the `edgesync hub serve` CLI. Wires them through and fixes a latent auth bug in seedFromUpstream that prevented it from cloning against the standard nobody-caps=gio deployment.

Changes: * cli/hub_serve.go — adds --project-code (40-char lowercase hex, format pre-validated for a friendlier error than libfossil's) and --seed-from-upstream (http(s) URL of upstream hub xfer endpoint). * hub/clone.go — drop bootstrapUser from the clone CloneOpts. Passing a local-creation user as the upstream-auth identity caused libfossil's clone to send a login card for a user the upstream had never heard of ("authentication failed"). Anonymous clone falls through to the upstream's "nobody" caps, matching the typical NobodyCaps:"gio" deploy. * hub/cross_leaf_test.go — adds TestCrossLeaf_SeedFromUpstream_PropagatesCommit covering the seed-then-sync path end-to-end (clone over HTTP, leaf-link, observe A→B commit propagation via NATS).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: e8190a881f user: [email protected] tags: trunk, v0.0.17

20:59
feat(cli): expose hub --project-code and --seed-from-upstream flags

The hub.Config.ProjectCode and hub.Config.SeedFromUpstream fields landed in #157 but were unreachable from the `edgesync hub serve` CLI. Wires them through and fixes a latent auth bug in seedFromUpstream that prevented it from cloning against the standard nobody-caps=gio deployment.

Changes: * cli/hub_serve.go — adds --project-code (40-char lowercase hex, format pre-validated for a friendlier error than libfossil's) and --seed-from-upstream (http(s) URL of upstream hub xfer endpoint). * hub/clone.go — drop bootstrapUser from the clone CloneOpts. Passing a local-creation user as the upstream-auth identity caused libfossil's clone to send a login card for a user the upstream had never heard of ("authentication failed"). Anonymous clone falls through to the upstream's "nobody" caps, matching the typical NobodyCaps:"gio" deploy. * hub/cross_leaf_test.go — adds TestCrossLeaf_SeedFromUpstream_PropagatesCommit covering the seed-then-sync path end-to-end (clone over HTTP, leaf-link, observe A→B commit propagation via NATS). leaf check-in: 001f82d3d2 user: 41898282+github-actions[bot]@users.noreply.github.com tags: trunk, feat/cli-project-code-and-seed-from-upstream

20:27
feat(hub): cross-leaf fossil sync over NATS (#156) (#157)

Add Config.ProjectCode + Config.SeedFromUpstream so multiple hubs sharing a project-code propagate commits over a single .sync subject. Adds an auto-publish notification on a sibling .commit subject + pull-on-notify subscriber, closing the cross-process Fossil sharing gap from #156.

- hub/nats_transport.go: libfossil.Transport over nats request/reply - hub/clone.go: HTTP-based SeedFromUpstream helper (NATS-clone deferred) - hub/repo.go: Repo.publish hook fired after successful Commit - hub/hub.go: startCommitSubscriber + Config plumbing + NoEcho - hub/cross_leaf_test.go: gold-path propagation, subject alignment, drift guard - libfossil pin -> v0.6.1 (CreateOpts.ProjectCode)

Design: docs/superpowers/specs/2026-05-12-cross-leaf-fossil-sync-design.md Protocol: docs/architecture/sync-protocol.md ("NATS-Wrapped Sync")

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 6f9e3e40d0 user: [email protected] tags: trunk

2026-05-11
17:56
refactor(hub): rename Hub.LeafUpstream() → Hub.LeafURL() (#155)

The getter returns the local leafnode listener URL — what remote leaves dial as their upstream. The name LeafUpstream collided with Config.LeafUpstream (the input URL THIS hub solicits to), making the API read backwards: same word, opposite directions on the same struct family.

LeafURL is symmetric with the existing NATSURL() and matches the data: "the URL of my leaf listener". Config.LeafUpstream stays — that's correctly named on the input side.

Mechanical changes: - (h *Hub) LeafUpstream() → (h *Hub) LeafURL() - private field h.leafUpstream → h.leafURL - hub_test.go callers updated - cli/hub_serve.go log key "leaf_upstream" → "leaf_url"

No callers outside this repo in the v0.0.16 dependency graph (bones pins v0.0.16). Downstream consumers on a local replace (sesh) get a one-line update.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 385682e7ce user: [email protected] tags: trunk

17:13
feat(hub): expose Hub.NumLeafs() for auto-shutdown semantics (#154)

Adds a thin getter over the embedded NATS server's NumLeafNodes(). Consumers that want to implement a "hub auto-shuts down when the last leaf disconnects" lifecycle can poll this — that's the immediate use case from the sesh wrapper spike, where the hub spawns on first session and exits on last.

Three lines, no behavior change.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: 4caf310202 user: [email protected] tags: trunk

15:26
feat(hub): machine-level hub CLI + ServerName / LeafUpstream config (#153)

Introduces `edgesync hub serve` and extends the hub package so a hub can be named at the NATS level and optionally solicit a leaf connection upstream. This is the substrate piece; session/agent vocabulary lives in a consumer (sesh) above this layer.

hub.Config: - ServerName — sets opts.ServerName on the embedded NATS server; surfaces in leafnode handshakes, varz, gossip. Empty leaves nats-server's auto-generated random name. - LeafUpstream — optional URL to solicit a leafnode connection toward. Wired to opts.LeafNode.Remotes. Lets one hub act as a leaf of another, enabling multi-hub topologies. - NATSStoreDir default renamed `nats-store` → `messaging` — implementation- agnostic on disk; contents underneath are still NATS JetStream format. The config field name stays `NATSStoreDir` (Go code is honest about what it is).

hub.Hub.ServerName() — getter returning the live identity (Config.ServerName or the auto-generated name).

cmd: `edgesync hub serve --name=<server-name> --leaf-upstream=<url> ...` defaults --name to <hostname>-hub and repo path to ~/.edgesync/<name>/hub.repo.

docs/architecture/hub-package.md updated to reflect the new fields, the messaging/ default, and a generic repo-file naming convention (the package itself doesn't impose a filename; the CLI defaults to hub.repo).

Verified end-to-end: - Hub solicits a hub: bidirectional pub/sub flows across two hubs on this machine via the leafnode handshake. - 3-edge star: 1 root hub + 3 leaf processes — fan-in, cross-leaf, subject filter all behave as expected. - ServerName visible at server-side truth (h.server.Name()). - 31 tests pass, vet clean.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> check-in: f3d5aa96bc user: [email protected] tags: trunk

2026-05-06
20:40
feat(hub): CommitOpts.Tags — attach tags at commit time (incl. branch=) (#148)

Plumbs libfossil's `Tags []TagSpec` primitive through `hub.CommitOpts` so callers can land a checkin on a non-current branch (or attach any other tag) without going around the EdgeSync API to libfossil.

Fossil branches are propagating tags: `branch=<name>` + `sym-<name>=*` together place a checkin on a branch. Exposing `Tags` rather than a git-flavored `Branch string` keeps the API fossil-native and is strictly more expressive — any tag (closed, release, custom) can be attached at commit time, not just branch creators.

Empty `Tags` preserves the existing trunk-default behavior; current callsites compile and run unchanged.

Closes #147 check-in: 7536beb856 user: [email protected] tags: trunk, v0.0.15

2026-05-05
15:42
fix(hub): periodic WAL checkpoint so vanilla fossil can read hub.fossil (#144)

Hub.fossil opens in WAL journal mode, so without checkpointing the on-disk file stays as a 4 KiB stub plus a multi-hundred-KiB *-wal sidecar — vanilla fossil tooling (fossil ui / fossil info / fossil timeline) refuses it because its validity probe stats the main file before consulting the WAL.

Wire a goroutine in NewHub that runs PRAGMA wal_checkpoint(PASSIVE) every Config.CheckpointInterval (default 10s) so vanilla fossil can always read hub.fossil while the hub is running. PASSIVE never blocks readers or writers. Hub.Stop drains the goroutine before closing the libfossil handle; libfossil v0.6.0's own (*Repo).Close runs a TRUNCATE checkpoint, so the post-Stop on-disk file is self-contained.

Closes #143. Bumps libfossil v0.5.0 → v0.6.0 (danmestas/libfossil#28). check-in: 72cfc528b2 user: [email protected] tags: trunk, v0.0.14

2026-05-04
22:05
feat: close three gaps blocking bones go.mod libfossil drop (#137) (#138)

* feat(leaf/agent): clone-if-absent via Config.CloneFromHubURL (#137 gap A)

Adds agent.Config.CloneFromHubURL — when set and Config.RepoPath does not yet exist on disk, agent.New clones the leaf repo from the hub HTTP URL before opening it. Matching project-codes between leaf and hub are required for downstream NATS sync subjects to align, so callers that need to bootstrap a leaf against an existing hub no longer have to import libfossil directly for the pre-agent clone step.

Also exposes agent.CloneRepo(ctx, repoPath, hubURL) as a standalone helper for callers that need a clone independent of agent construction.

Existing behavior is preserved: when CloneFromHubURL is empty, agent.New errors on missing RepoPath as before (covered by TestAgentNew_RejectsMissingRepoWithoutCloneURL).

Closes part of #137 (gap A — bones internal/coord/leaf.go).

* feat(hub): subscribe to fossil-sync NATS subject in NewHub (#137 gap B)

NewHub now connects a local NATS client to its embedded server and subscribes to "fossil.<project-code>.sync" so peer leaf agents can push xfer requests to the hub over NATS — alongside the existing HTTP path. Previously, achieving this required running a leaf-agent in serve mode on top of the hub (Pull=false, Push=false, Autosync=AutosyncOff, ServeNATSEnabled=true), which transitively pulled libfossil into the consumer's import graph.

The subscriber is enabled by default. Read-only HTTP-only mirrors that shouldn't accept NATS pushes can opt out via Config.DisableFossilSyncOverNATS.

Note on naming: the issue brief proposed Config.ServeFossilSyncOverNATS defaulting to true. Go bool zero-value semantics make default-true awkward without pointers, so the inverted-name shape (DisableX, defaults false = enabled) was used instead.

Closes part of #137 (gap B — bones internal/coord/hub.go).

* feat(hub): FileToCommit.Perm + CommitOpts.Time pass-through (#137 gap C)

Adds two additive fields to the public hub commit surface so consumers no longer need libfossil-direct access to preserve executable bits or seed reproducible commit timestamps:

- hub.FileToCommit.Perm — "x" marks the file executable on the resulting manifest's F-card. Empty preserves prior behavior. Other libfossil perm markers pass through verbatim. - hub.CommitOpts.Time — explicit commit timestamp; zero defaults to time.Now().UTC() at commit time, matching the prior behavior.

Tests cover round-tripping the executable bit via libfossil.Repo.ListFiles, honoring an explicit time via Timeline, and falling back to "now" on the zero value.

Closes part of #137 (gap C — bones internal/hub/hub.go seed path).

* ci: include #137 sim tests in the serve-tests allowlist

Adds TestAgentNew_ and TestHubNATSFossilSync_ to the sim serve-tests regex in both Makefile and test.yml so the new clone-if-absent and hub-NATS-subscriber tests actually run in CI alongside TestHubLeafE2E. check-in: ad1d55f5b9 user: [email protected] tags: trunk, leaf/v0.0.10

20:59
feat(hub): SetUserCaps + Config.NobodyCaps for unauth-clone hubs (#135)

Adds Repo.SetUserCaps(login, caps) and a Hub delegate, wrapping libfossil's Repo.SetCaps. Lets consumers grant or change caps on existing users without leaving the hub package — primarily for the common bones case where the libfossil-pre-populated 'nobody' user needs 'gio' to allow unauthenticated clone/pull/push.

Also adds Config.NobodyCaps so the typical setup is one declarative field instead of a manual SetUserCaps call after every NewHub. NobodyCaps is applied at bootstrap by calling SetUserCaps("nobody", v) internally — no method-body duplication. Empty NobodyCaps leaves nobody at whatever libfossil set it to (idempotent re-open path preserves prior caps, doesn't overwrite).

Tests: - TestRepo_SetUserCaps_RoundTrip — set + read back - TestRepo_SetUserCaps_RejectsEmptyLogin - TestNewHub_NobodyCaps_AppliedAtBootstrap — Config field works - TestNewHub_NobodyCaps_Idempotent — re-running with same caps OK - TestNewHub_NobodyCaps_EmptyDoesNotOverwrite — empty doesn't reset

Closes #130 check-in: 93ab4aa0e9 user: [email protected] tags: trunk, leaf/v0.0.9

20:53
feat(hub): SetUserCaps + Config.NobodyCaps for unauth-clone hubs

Adds Repo.SetUserCaps(login, caps) and a Hub delegate, wrapping libfossil's Repo.SetCaps. Lets consumers grant or change caps on existing users without leaving the hub package — primarily for the common bones case where the libfossil-pre-populated 'nobody' user needs 'gio' to allow unauthenticated clone/pull/push.

Also adds Config.NobodyCaps so the typical setup is one declarative field instead of a manual SetUserCaps call after every NewHub. NobodyCaps is applied at bootstrap by calling SetUserCaps("nobody", v) internally — no method-body duplication. Empty NobodyCaps leaves nobody at whatever libfossil set it to (idempotent re-open path preserves prior caps, doesn't overwrite).

Tests: - TestRepo_SetUserCaps_RoundTrip — set + read back - TestRepo_SetUserCaps_RejectsEmptyLogin - TestNewHub_NobodyCaps_AppliedAtBootstrap — Config field works - TestNewHub_NobodyCaps_Idempotent — re-running with same caps OK - TestNewHub_NobodyCaps_EmptyDoesNotOverwrite — empty doesn't reset

Closes #130 leaf check-in: a8e62ce82d user: [email protected] tags: trunk, feat/130-hub-setusercaps

20:51
feat(hub): add OpenRepo + *hub.Repo type for server-less operations (#134)

Adds hub.OpenRepo(path) (*Repo, error) — a server-less handle for one-off CLI invocations and short-lived helpers (bones #186 needs this for `bones hub user list`, `bones plan finalize`, and slot-user provisioning, all of which currently drop to direct libfossil.Open because hub.NewHub spins up a redundant NATS server every time).

The new *hub.Repo type holds the libfossil handle and carries every user / commit / read method that *hub.Hub used to define on itself. *hub.Hub now holds a *hub.Repo internally and one-line-delegates each method, so both code paths share method bodies — no duplication.

Adds Hub.Repo() *Repo accessor for sharing the live handle out of a running hub. Documented contract: don't call Close on a *Repo returned by Hub.Repo while the Hub is still serving — Hub.Stop is the right teardown for that case. Repo.Close is idempotent so accidental double- close from teardown ordering is safe.

Tests: - TestOpenRepo_RoundTrip — every *Repo method against a path bootstrapped by NewHub. Same observable state. - TestOpenRepo_RejectsEmptyPath, TestOpenRepo_RejectsMissingPath. - TestRepo_Close_IsIdempotent. - TestHub_Repo_AccessorReturnsLiveHandle — operations on the *Repo accessor are observable via the *Hub.

Closes #131 check-in: 5f025def67 user: [email protected] tags: trunk

19:17
fix(leaf,hub): drop SetMaxOpenConns(1) to unblock concurrent clones (#122)

The applySQLiteTuning helper added in #108 (and propagated to hub/ in #109) capped the libfossil DB connection pool at 1 connection. The intent was to make libfossil's per-connection PRAGMAs stick, but libfossil's clone path has internal goroutines that all need a DB connection — capping the pool deadlocks every concurrent clone, with one goroutine holding the conn while another waits for it.

Bones reported (issue #120) that bumping leaf v0.0.6 → v0.0.7 deterministically deadlocks examples/hub-leaf-e2e/'s 3-leaf concurrent clone test. Reproduced here in hub/clone_concurrency_test.go: 3 parallel libfossil.Clone calls against a shared hub. Without the fix the test hangs until its 25s timeout. With the fix it completes in ~80ms.

Keeping the busy_timeout=30000 PRAGMA — that's the correct primitive for concurrent-write safety; SQLite retries SQLITE_BUSY internally up to the timeout. The pool cap was always overkill.

If we later need PRAGMA stickiness across pooled connections, route it through the modernc driver's per-connection init hook instead of a pool cap.

Closes #120 check-in: b251b87613 user: [email protected] tags: trunk, leaf/v0.0.8

18:34
feat(hub): new package for hub-fossil management and HTTP serving (#109)

Adds a top-level hub/ package that hosts an EdgeSync hub in-process: fossil hub repo bootstrap, embedded NATS server with JetStream, auto-port allocation, libfossil HTTP server (timeline UI + xfer endpoint), user management, and hub-side commits/reads. All libfossil and nats-server types are wrapped internally — the public surface exposes only stdlib types and types defined here.

Public API: - Config + NewHub + ServeHTTP + Stop - NATSURL / LeafUpstream / HTTPAddr accessors - AddUser / GetUser / HasUser / ListUsers / RemoveUser (User type) - Commit / Read / ReadAt (RevID, FileToCommit, CommitOpts types)

Auto port allocation: passing 0 for FossilHTTPPort, NATSClientPort, or NATSLeafPort yields three distinct bound ports. The HTTP listener is bound during NewHub so HTTPAddr() is immediately usable, even before ServeHTTP is called.

SQLite tuning (SetMaxOpenConns(1) + busy_timeout=30s) applied at hub bootstrap, matching the leaf agent's tuning.

Lives inside the EdgeSync root module (not a separate go.mod) so it ships in lockstep with the leaf agent. Can be promoted to its own module later if downstream consumers need to depend on it independently.

Closes #102 check-in: 67ea682587 user: [email protected] tags: trunk