Skip to content

Online First

easylogic edited this page Oct 15, 2026 · 8 revisions

Online-First

Status: protocol proposals accepted by Review in #47/#48; escalations decided in D-052..D-056.

Repository-Semantics §6: online-first, CRDT ops, thin clients. Issue #47. Input decision: D-048 (product direction B). This page is the main spec; it extends Repository-Semantics §1–§4 and Remote-Sync §5 and does not change their convergence rules. Vision, Architecture and Migration-Path were updated to direction B in the same change.

Labels: [decided] (with a D-link), [decision-proposal] (this spec's proposal; Review/Mentor may accept), [open question], [escalation] (a product choice for the human; not decided here). No code was changed for this page. Code references are to develop 714faf0 (crates/hub-core, crates/hub-cli), plus hub/issue-49 where noted.

6.0 Summary

  • [decided] (D-048) The server is the authoritative, always-on replica. Clients hold only what they need and send ops in real time. The op model and convergence stay: no merge, no merge queue. Short disconnects are buffered and resent. Rejected: A (offline-first full replicas as the default), C (server-side merge, Scion-like).
  • [decided] (D-038) One central replica that everyone syncs with. D-048 strengthens this: the central replica is now also the source of derived data and the API endpoint.
  • [decision-proposal] "Authoritative" means availability, durability, admission and derived data, never ordering. The server does not assign op ids, does not sequence ops and does not resolve conflicts differently from any other replica. Ops(A) == Ops(B) ⇒ State(A) == State(B) (§3.5) still holds for every replica, including the server and every thin client over the same op set.
  • [decision-proposal] Wire protocol v1 stays (protocol 1); everything new is behind Hello features (live, partial, lthash), as 5.2.2 requires. No on-disk format major bump for full replicas; a thin client has its own layout (6.2.6).

6.1 The server: the authoritative replica

6.1.1 Role

  • [decision-proposal] The server is a role played by a full replica, not a different kind of replica. Any full replica (a bare repo, D-042) can be started as a server. A thin client can never serve (it lacks the ops and blobs it would have to answer for). So "symmetric protocol" (5.1) remains true between full replicas, and is refined: a partial replica only ever talks to a full replica that serves it. [decided] (E-B5, D-052): whether to also allow a client-side full replica to be promoted to the server for a team (failover), or keep exactly one server per repo.
  • [decision-proposal] The server accepts ops; it never assigns them. Every op is authored with the author's own ReplicaId and a Lamport counter above its deps (§2.1), as today. Reasons: (1) an author must know its op's id before the ack, to build the next op on it (the real-time path would otherwise block on a round trip per edit); (2) server-assigned ids would make the server's ack a precondition of convergence, which is the merge-queue C rejected; (3) the D-008/D-022 checks stay identical. The only exception is the hosted replica of the HTTP API (6.5.3), where the server authors on the caller's behalf, under a replica id bound to that caller.
  • [decision-proposal] An ack is the existing Committed <frontier> (5.4): the server's durable frontier after persisting, with R lines for rejected ops (5.2.7, D-045). The server's version after an ack is a version id of that frontier (D-005), so "the server's version" is the same notion as every other version.
  • [decided] (D-043) --read-only stays; under 6.5 it becomes a per-principal scope.

6.1.2 Durability and A.1 on the server

  • [decision-proposal] The server persists every accepted batch with the A.1 barrier order before acking it (5.4 unchanged). In live mode it group-commits: batches arriving within commit_window = 5 ms (or 256 ops) share one barrier, as #29's group commit. One fsync per window is what makes the 6.3 latency target compatible with A.1 on a laptop SSD and on a server disk.
  • [decision-proposal] Single-writer daemon. Live mode needs the server to keep the repo open in memory: today's serve --stdio reopens the state at every persist step (R1 "persist-only lock plus reload-and-merge", 5.3), which costs a full load per batch and cannot meet 6.3. So the server is a long-running process (barocss-hub serve --http, 6.5) that holds the exclusive repo lock for its lifetime and is the only writer. serve --stdio sessions on the same repo (ssh full-replica clients) connect to the running daemon over a local socket instead of opening the repo themselves; without a daemon they keep today's behavior. A local record on a non-bare server repo goes through the daemon as well. This supersedes the 5.3 R1 lock model for a daemon-served repo (it was a decision-proposal, not a D-entry).

6.1.3 Derived data, computed incrementally

Scion's "derived data computed from parents" maps to barocss-hub versions: every derived value is a function of a version (§3.3), and the server maintains it for its head incrementally per applied op, plus for bases (6.2.2) when it cuts one.

derived value today (develop) on the server label
state (tree) hub-core::tree Replica apply, incremental per op; #33 snapshots unchanged; snapshot every snap_min ops (A.2) [decision-proposal] reuse
conflicts recomputed by evaluate per op set; #28 removed O(n²) maintained per applied op (the apply already sees each conflict) [decision-proposal]
projection, name collisions, short ids projection::project over a state, range-based short ids (#31) recomputed per changed directory only (dirty set from the applied ops' parents) [decision-proposal]
version ids checkpoint::version_id from the frontier computed per ack reuse
per-author digests digest::Sums (SHA-256 sequence) per session; digest::LtSums (feature lthash) LtSums of the head, += E(h(o)) per applied op (one 2.1 µs hash per op, Performance #46), kept in memory [decision-proposal]; persisting it is a format change (D-051 "no on-disk LtHash cache"), so it is rebuilt at daemon start
history index log --path walks the op log per NodeId: the list of op ids touching it (Create/Move/Delete/SetContent), appended per op; path → NodeId at a version via the state [decision-proposal] new
blame none per-file op history now; line blame after the #48 text CRDT (6.6) [open question] for #48
bases none a snapshot of the state at a published frontier, with VV, LtSums and the byte size of each blob [decision-proposal] new (6.2.2)
  • [decision-proposal] Derived data is a cache: it is never replicated, never authoritative over the op set, and must equal a from-scratch recomputation (property 6.10 #9). This is what keeps the server "just a replica" for convergence.

6.2 The thin client: a partial replica

6.2.1 What a thin client holds

  • [decision-proposal] A thin client holds:
    1. a base B: a server-provided snapshot at version v_B (6.2.2);
    2. a window W: the full bodies of every op it received or authored above B (B ∪ W is down-closed, as every version is);
    3. its own-op ledger (6.2.3);
    4. its outbox: own ops not yet covered by a server ack (6.4);
    5. a blob cache, bounded (6.2.5), plus the working directory of its sparse set;
    6. local config, trust store (D-046), warnings, reported rejections (D-050).
  • [decision-proposal] Op metadata is windowed, not full: the thin client does not hold op bodies below B. Full history, blame and old versions are server API reads (6.5). Rationale: at D-026 scale (1M ops) the op log is ~100–200 MB and the full state is needed anyway; a base snapshot is the state without the history.
  • [decision-proposal] The thin client holds the whole tree metadata (all nodes, names, parents, blob ids) of B ∪ W, not just its sparse set. Reasons: (1) applying a Move/Delete/Create needs the parents and the cycle rule (§2.4) over the whole tree; (2) name collisions and projection are per directory but a move can bring any node in; (3) the cost is the snapshot (#33: well under the op log). Sparseness applies to content (blobs) and materialization, not to tree state.

6.2.2 Bases

  • [decision-proposal] A base is cut by the server at a frontier it has made durable, at most every base_every = 10k ops or on a published checkpoint. It is the tree_snapshot encoding (#33, Replica::encode_snapshot) plus: the frontier, the exact membership M(B) (below), the per-author LtSums D_r(B) with counts, the conflicts, and the size of each blob. Its id is the version id of its frontier.
  • [decision-proposal] Exact membership (Review R1). A version vector does not describe a version: own-chain gaps are accepted (§2.6 #7, 4.4/4.5), so B can hold (6,r) without (5,r). Deciding "below B" by VV(B) would let a thin client apply an op that depends on (5,r) while a full replica holding exactly B keeps it pending, breaking 6.10 #1. So a base ships M(B): for each author r, the exact set of counters c with (c, r) ∈ B, encoded as sorted run-length ranges (VV(B)[r] is just its maximum, and the gaps between ranges are the holes). Size ≈ 8 bytes per range; an author without own-chain violations whose ops all lie in B costs ranges only where its Lamport counters jump.
    • id ∈ B ⇔ id.counter is in r's range list. This exact test, never VV, is used for causal readiness, for K (6.7), for causally_below on ids below the base, and for the #48 epoch and element membership (Text-CRDT 7.4.2/7.4.3: whether an epoch op or an inserting op of an element is in V = B ∪ W at the thin client).
    • At rebase, M(B') replaces M(B); ids of W that are in M(B') are dropped from the window.
  • [decision-proposal] Rebase: when a newer base B' with B ⊆ B' ⊆ B ∪ W ∪ (ops the client lacks) exists, the client fetches it, checks D_r(B') == D_r(B) + D_r(W ∩ B') + D_r(fetched ops) for every author (6.7), drops W ∩ B' from the window, and keeps W \ B'. A client never rebases over an outbox op the server has not acked (by construction B' only contains server-durable ops, so an unacked op is never in B').
  • [open question] Whether bases are cut only at published checkpoints (fewer, user-visible) or also automatically.

6.2.3 What must stay local for the invariants

The rule: a thin client must be able to run every §4.5 check that applies to ops it receives or authors, without asking the server. The server's checks are a second line, not a replacement (a server could be compromised, 5.9.4).

check needs thin-client source label
Lamport rule (§2.1, deps < op) only op ids of the deps the op itself (OpId = (counter, replica), ordered by counter then replica) [decision-proposal]; no base ops needed
causal readiness (deps present, else pending) membership of each dep in B ∪ W the exact membership M(B) (6.2.2) for ids in B, never VV(B); W for the rest; a dep in neither → pending and Need [decision-proposal]
equivocation in W (D-008) the held payload of the same id W reuse hub-core::sync receive
equivocation against B the payload of an id below B not held; covered by the digest D_r(B) + D_r(W) vs the server's (6.7) [decision-proposal]
own-id op not authored here (DuplicateReplica, D-022) the set of own op ids and their hashes the own-op ledger: (counter, h(o)) for every op ever authored here, 40 bytes per op, never evicted [decision-proposal]
never reuse an own counter the highest own counter the ledger [decision-proposal]
authoring a new op on the index base (#25: the version the user last checked out, ⊆ B ∪ W; never the frontier, Text-CRDT 7.4.3, 6.10 #20) the index base and the max counter seen B ∪ W reuse SyncReplica authoring
blob hash on receipt (D-021) the blob bytes on demand (6.2.4) reuse receive_blob
  • [decision-proposal] Blob-remote rule (refines §4.5). Today an op that names a missing blob stays pending (lacking_blobs). A thin client instead applies such an op when the server's Committed frontier covers it: the server holds the blob (A.1 clause 2: it never makes an op durable without its blob), so the blob is remote-held. State and conflicts depend on the blob id, not its bytes, so convergence is unaffected. The bytes are fetched when the file is materialized. A full replica keeps today's rule. This is a refinement of §4.5 for partial replicas, recorded here because it changes when an op is applied.

6.2.4 Blobs on demand, sparse and virtual checkout

  • [decision-proposal] Sparse set: a list of directory NodeIds (not paths, so a renamed or moved directory stays in the set; §1.1). barocss-hub sparse add|rm|list <path> resolves a path at the current version to its NodeId. Only nodes inside the sparse set are materialized; their blobs are fetched with NeedBlobs (existing frame) when an op makes them current. Default sparse set: the whole tree (behaves like today for small repos). [decided] (E-B3, D-052) covers the size defaults.
  • [decision-proposal] Never dematerialize a dirty file (Review R4). When a materialized file would leave the materialized area (an incoming Move out of the sparse set, a Delete, or sparse rm), and its working file has unrecorded edits (differs from the indexed blob), the client does not remove it. The path stays on disk as blocked, exactly as #20's blocked state (checkout safety: never overwrite an unrecorded file), shown by status / status --json (blocked, reason dirty-outside-sparse) and a watch event. record records it first (at its node, wherever the node now is), after which it is dematerialized normally; or the user adds the directory back to the sparse set. sparse rm refuses with the list of dirty paths unless they are recorded first.
  • [decision-proposal] Outside the sparse set nothing is written to the working directory. A record never sees those paths, so it never emits Deletes for them (the wd scan must treat "not materialized" as "absent by choice", not "deleted"; this is the one wd change thin clients need).
  • [decision-proposal] Virtual checkout (materialize on open) is optional and per integration: the VS Code extension's FileSystemProvider first (no kernel driver, fits #41), OS-level virtual filesystems (FUSE/FSKit, ProjFS) later. [decided] (E-B6, D-052): which, if any, ships in the alpha.
  • [decision-proposal] Prefetch: when an op arrives for a materialized file, the client fetches its blob immediately (it is needed); blobs outside the sparse set are never fetched unless read.

6.2.5 Bounded cache and eviction

  • [decision-proposal] The cache is .hub/blobs/ on a thin client, with a byte budget cache_max ([decided] (E-B3, D-052) default; proposal: 2 GiB or 10 % of free disk, whichever is smaller).
  • Never evictable (pinned): blobs named by an outbox op (unacked own content); the content of a materialized path whose working file differs from the indexed blob (a dirty file needs its base for status); blobs of paths the user pinned for offline (sparse pin). Text seeds (#54 Review, Text-CRDT 7.11, #47 E-B3) [decided: stated rule; implemented in O2]: the epoch seed blob of every text doc referenced by .hub/snapshot (snapshot v2 references seeds by blob id) or held by a live TextDoc is pinned. Evicting one requires first invalidating (removing) the snapshot that references it and dropping the doc.
  • Evictable: everything else, least-recently-materialized first. An evicted blob is remote-held; re-reading fetches it again.
  • Window ops below a base are dropped at rebase (6.2.2); the ledger is never evicted; warnings are kept as today.
  • [decision-proposal] Eviction never changes state: it removes bytes whose id is still in state. Property 6.10 #6.

6.2.6 Thin-client layout

  • [decision-proposal] .hub/ with a marker partial (like the #49 v1-rescue replica marker), base/<version-id> (snapshot), ops.log holding W only (format v1 batches), ledger (own ops), outbox is not a separate file (it is the own ops of W not covered by server-acked), server-acked (the last Committed frontier), sparse (NodeIds). A pre-#47 binary must refuse a partial repo (corrupt: unknown marker), as a newer major format is refused today (5.2.3).

6.3 The real-time write path

6.3.1 Flow

  1. The editor saves. The VS Code extension sends a saved <path> hint to the watcher over the existing watch control channel; other editors are seen by polling (100 ms, watch::Opts::poll).
  2. The watcher records immediately on a hint (no debounce) and after the debounce otherwise (400 ms today). record produces ops whose deps are the index base (#25, as develop's prepare_on(idx.base): the version the working directory was last checked out at), not the current frontier, and appends them durably (A.1). A remote op that arrived but was not yet materialized is therefore concurrent with the save (a remote SetContent stays a visible content loser or winner, never silently overwritten; Text-CRDT 7.4.3). A live client materializes incoming ops promptly (6.3.1 step 5), so its index base is normally its frontier.
  3. The live session (feature live) sends an unsolicited Ops frame with the new ops, followed by BlobBegin…BlobEnd for new blobs up to inline_blob = 1 MiB (pushing them avoids a NeedBlobs round trip). Larger blobs wait for the server's NeedBlobs.
  4. The server validates (§4.5), group-commits (6.1.2) and replies Committed <frontier> (plus R lines). The client records the frontier in server-acked; the outbox shrinks.
  5. The server forwards the ops in one Ops frame to every other live session. Each client applies them (6.2.3 checks), updates its state, and materializes affected files in its sparse set (fetching blobs on demand).
  • [decision-proposal] In live mode both sides may send Ops at any time after the initial pull and push halves; each Ops batch from a client is answered by Committed, and each forwarded batch to a client is answered by the client's Committed, so the server knows what each client holds (no Have per edit). A Ping/Pong frame pair (new types, feature live) every 20 s keeps NATs open; the 1 h session wall clock (5.2.5) does not apply to live sessions, the 60 s step timeout applies to an outstanding Committed only.
  • [decision-proposal] Live mode replaces the --sync 2 s polling interval of watch for remotes that announce live; others keep polling.

6.3.2 Latency target

  • [decision-proposal] "Visible" = the op is applied in another online client's state and its watch emits the event (materialization of the file follows, plus blob transfer time).
    • LAN (RTT ≤ 5 ms): save → visible p50 ≤ 150 ms, p99 ≤ 300 ms for a save with a hint and a blob ≤ 1 MiB. Budget: record ≤ 50 ms (one durable append), client → server ≤ 5 ms, group commit ≤ 5 ms + fsync, forward ≤ 5 ms, apply ≤ 10 ms. Precondition: this needs a hint-targeted record (record only the hinted path, not a whole-tree scan) and a resident repo in the watcher (state kept in memory, no reload per record); today's record costs 0.34–0.93 s per call at D-026 scale (Performance #32), which alone exceeds the target.
    • WAN: LAN budget + 2 × RTT to the server.
    • Without a hint (polling editors): + poll + debounce (≤ 500 ms with today's defaults).
  • [decided] (E-B7, D-052) The numbers (the Issue's example is ≤ 300 ms on LAN). Today's path is ≥ 2.4 s (debounce 400 ms + --sync 2 s); this target needs live mode, the hint, and the daemon (6.1.2).

6.4 Short disconnects

  • [decided] (D-048) Ops are buffered and resent; convergence is unchanged.
  • [decision-proposal] While disconnected, record keeps authoring on the index base (#25, ⊆ B ∪ W; equal to the local frontier when the working directory is at head); ops go into the outbox (they are already durable in ops.log). Reconnect = a normal session: Hello, pull half, push half (5.2.4), then live. Resend is exactly today's push half: the server answers Have with V_S \ K, the client sends the outbox ops the server lacks. Nothing is new about convergence: the outbox ops are concurrent with whatever the server received meanwhile, and §2/§3 decide the result. Backoff on reconnect as watch does today (2^min(n,5) intervals).
  • [decision-proposal] What does not work offline on a thin client: reading a file whose blob is not cached (error offline-not-cached, naming the path); history/blame below B; sparse-set changes that need new blobs. Everything else (record, status, log of W, checkout of cached files) works.
  • [decision-proposal] Buffer limit. The outbox is bounded by offline_max_ops = 100k ops, offline_max_bytes = 1 GiB of new blob bytes, and offline_max_age = 7 days. Past any limit, a thin client refuses new records (watch stops recording and reports offline-limit; files keep their edits in the working directory, so nothing typed is lost, it is just not recorded) until it reconnects. Reason: the limit bounds the work of the reconnect (D-026's "sync after 1 week offline ≤ 1 min" stays the bound) and keeps the online-first premise honest.
    • Alternative (a): degrade to a full replica. Not possible while offline (the missing ops and blobs are on the server). It is offered before going offline: barocss-hub hydrate turns a thin client into a full replica while online (fetch all ops below B and all blobs), after which the full-replica rules apply and there is no offline limit.
    • Alternative (b): no limit. Rejected by this proposal: an unbounded outbox makes the reconnect cost unbounded and hides a client that is effectively offline-first.
  • [decided] (E-B2, D-052) The offline limit (numbers) and the behavior past it (refuse new records, as proposed; or keep recording without bound).
  • [decided] (E-B4, D-052) Whether full replicas remain supported for users (hydrate, today's clone). Proposal: yes, opt-in, because the server is itself a full replica, because D-026's offline target is measured on full replicas, and because the existing code is the full-replica code.

6.5 Server API for agents and CI

6.5.1 Transport

  • [decided] (D-037) HTTP comes after SSH. D-048 makes it the primary transport for thin clients and agents.
  • [decision-proposal] barocss-hub serve --http <addr> --root <dir> (the daemon, 6.1.2), TLS required except on loopback. Endpoints under /v1/repos/<repo>/:
    • POST sync: wire v1 frames, as designed in 5.3.2 (one request per session; HTTP/2 or chunked bodies). Full replicas use this.
    • GET live (WebSocket, binary messages = v1 frames): the live session (6.3) for thin clients.
    • The JSON API below, for agents and CI with no clone. Documents reuse the CLI-JSON schema 1 shapes (version, node objects, conflicts, warnings), with a top-level "schema": 1.
  • [decision-proposal] One repo per URL path, confined under --root like 5.9.2; no repo listing unless the principal is allowed.

6.5.2 Read (no clone)

request answer from
GET versions/<v>/tree?path=<p>&depth=<n> nodes with NodeId, kind, name, blob id, size, conflicts state at v (head: in memory; older: nearest base + replay)
GET versions/<v>/files/<path> or GET nodes/<id>/content?v=<v> the bytes; headers Hub-Node, Hub-Blob blob store
GET diff?from=<v1>&to=<v2>&path=<p> node-level changes (create, move, delete, content) like status --json two states
GET history?path=<p> or GET nodes/<id>/history ops touching the node, following moves (NodeId, not path) history index
GET blame?path=<p> per-file op history now; per line after #48 [open question] for #48
GET conflicts?v=<v> conflicts of the version derived
GET status server head version, frontier, warnings visible to the principal, live session count derived
GET checkpoints, GET refs published checkpoints (refs when §3.8 is implemented) published.log
  • <v> is a version id, a short id (D-005), a checkpoint name, or head. Every answer carries the version id it was computed at, so an agent can write against it.

6.5.3 Write (no clone)

  • [decision-proposal] POST ops with {"base": "<version>", "ops": [ {"op":"create","kind":"file","path":"src/x.rs","blob":"<sha256>"} | {"op":"mkdir","path":…} | {"op":"move","node":"<id>"|"path":…,"to":"<path>"} | {"op":"delete",…} | {"op":"set-content",…,"blob":…} ]}, header Idempotency-Key. Keys are scoped per (principal, repo, key); a retry with the same key and the same canonical request body returns the stored answer, a different body under the same key is 409 idempotency-conflict and authors nothing. Blobs are uploaded first with PUT blobs/<sha256> (verified on receipt) or inline as base64 up to 1 MiB.
  • [decision-proposal] Hosted replica. The server authors the ops under a replica id it allocates and binds to the principal (one per principal and agent session, recorded server-side). The deps are the frontier of base (the request's required base, the API's index base), not the server head (6.10 #20). So an agent that read version v and writes "on v" gets exactly the concurrency semantics a human client gets: its edit is concurrent with everything recorded after v, and §2/§3 converge it (move vs delete per D-001, revive per D-023, content conflicts per §2.3 until #48). base is required; a missing base is 400 usage (writing on "whatever head is now" would make the result depend on timing).
  • Paths are resolved to NodeIds at base (paths are not identity, §1.1); a path that is ambiguous at base (a collision name~shortid) must be given by node id or its suffixed form.
  • The answer: {"ops": ["<counter>@<replica>", …], "version": "<server head after the ack>", "conflicts": [new conflicts caused], "rejected": []}, sent after the A.1 persist. A retry with the same Idempotency-Key returns the same op ids and authors nothing new (the key → op ids map is kept 24 h).
  • [decision-proposal] Agents that want the full guarantees offline-tolerant (a long-running coding agent in a container) run a thin client instead (barocss-hub clone --thin), exactly like a human. Both paths produce ordinary ops: same convergence, same digests, same warnings.

6.5.4 Authentication and authorization (E-R3, now required)

  • [decided] (D-037) SSH remotes keep SSH authentication (5.9.6).
  • [decision-proposal] HTTP requires authentication on every request except loopback with --insecure-local. Every principal has scopes read, write, admin per repo; serve --read-only (D-043) caps every principal at read.
  • [decision-proposal] Replica binding by enrollment, not first push (Review R3). First-push-wins would let anyone claim a colleague's existing replica id after the upgrade (denial of service, and authorship spoofing for later ops). Instead:
    • Enrollment of a new replica: an authenticated principal P calls POST enroll (CLI: done by clone --thin, remote add against an HTTP remote, or barocss-hub auth enroll). The server returns a fresh random replica id (128-bit, never used) bound to P; the client adopts it (a fresh .hub/ takes it at creation; an existing repo with a never-used id may adopt it; one with ops does reid semantics: new ops under the new id, old ops stay under the old one). Enrollment can only bind ids with no ops on the server, and only ids the server itself issued, so no existing id can be claimed this way.
    • Existing replicas (migration): at the first daemon start the server lists every replica id with ops (serve admin replicas), all unbound. Ops under an unbound id are rejected with unbound-replica (D-008 warning, exit 3) until an admin binds it: barocss-hub serve admin bind <replica> <principal> (or a mapping file replica-bindings under the server's .hub/, read at start). The owner can always escape without an admin by enrolling a fresh id (their next ops are under it; history stays).
    • Ops under an id bound to another principal: foreign-replica (D-008 warning, exit 3 per D-045). Hosted API replicas (6.5.3) are enrolled implicitly per principal and agent session.
    • The binding is server-side metadata; the op format does not change. It turns the unauthenticated replica-id claim (5.2.3 R5) into an authenticated one.
  • [decision-proposal] SSH principals and the daemon. The daemon listens on .hub/daemon.sock (mode 0600, owned by the server OS account). A forced command names the principal: command="barocss-hub serve --stdio --principal alice --root /srv/hub --from-ssh-original-command"; serve --stdio connects to the socket and passes the principal in a local handshake (the daemon trusts it because only the server account can open the socket; anyone with a shell as that account is an admin, documented). Without --principal, a daemon-served repo accepts only reads from that session. The daemon holds the repo's exclusive .hub/lock for its lifetime, so a pre-#47 binary in an old forced command (which opens the repo itself) gets locked and writes nothing behind the daemon's back.
  • Options for the mechanism ([decided] (E-B1, D-052), the human chooses):
    1. Bearer tokens over TLS: server-issued, scoped, revocable personal and agent tokens (barocss-hub auth token create --scope write). Simplest for CI and agents; tokens are secrets to store.
    2. SSH-key challenge → short-lived token: barocss-hub auth login signs a server challenge with the user's SSH key (ssh-agent, ssh-keygen -Y sign), the server checks it against authorized_keys-style entries and returns a token valid for hours. Reuses the SSH trust users already have; agents still need option 1.
    3. mTLS client certificates: strong, no bearer secret on the wire; certificate issuance is operational work.
    4. OIDC/SSO via a reverse proxy (the proxy authenticates, passes a signed identity header): fits organizations; needs a proxy.
    • Proposal: 1 + 2 for the alpha, 4 as a deployment option later.

6.6 Hook-in point for the text CRDT (#48), interfaces only

Nothing here chooses the CRDT; #48 does: see Text-CRDT (§7), which answers 6.13's #48 questions (7.5.2 Q3, 7.7 Q4, 7.10 Q5, 7.9 Q6, 7.11 Q1/Q2) and replaces seed(blob) by the virtual seed (7.4.2). The interfaces below are where it plugs in, so #47's work does not block on it.

  • Op model: new Payload variant(s) for text edits (or a per-file sub-log; #48 decides), authored with ordinary OpIds and deps, so they flow through Ops frames, Lamport checks, digests (h(o) covers them) and acks unchanged.
  • Content model trait (in hub-core, sketch):
    • fn is_text(node, attrs) -> bool (#48's rule);
    • fn diff(prev_materialized: &[u8], saved: &[u8]) -> Vec<TextEdit> (deterministic; #48 picks the diff);
    • fn apply(state: &mut TextState, op: &Op); fn materialize(state: &TextState) -> Vec<u8>;
    • fn seed(blob: &[u8]) -> TextState (first text edit after an upgrade seeds from the current blob).
  • Write path (6.3): record calls diff for a text file instead of emitting SetContent.
  • Server derived data (6.1.3): a per-node TextState at head, maintained per op; bases carry it for text nodes.
  • Thin client (6.2): "blob on demand" becomes "content on demand": for a text file the unit fetched is its TextState at B plus the text ops in W, because a client must hold the sequence state of a file to author a mergeable edit on it.
  • API (6.5.3): set-content on a text file is diffed by the server against the file's text at base, so two agents writing whole files on the same base merge instead of conflicting.
  • Open questions handed to #48 are listed in 6.13.

6.7 Partial-replica digest: LtHash16, D_r(B) + D_r(W)

  • [decided] (D-051) Full replicas use the SHA-256 sequence digest on the wire; LtHash16 (digest::LtSums, n = 1024 u16 lanes, BLAKE3-XOF elements) is reserved for partial replicas and goes on the wire behind its own Hello feature.
  • [decision-proposal] Feature lthash (only when both sides announce it; a thin client requires it). In the Sums frame (5.2.8) the per-author checksum is the 32-byte BLAKE3 checksum of the LtHash16 value D_r(K) instead of S_r(K).
    • With partial, Have also names the client's base, and K = B ∪ downclose(resolved), where B is taken by its exact membership M(B) (6.2.2); the VV hint is never used to compute K (it stays a hint for the delta, 4.3).
    • The client knows D_r(B) (shipped with the base and checked at fetch, below) and computes D_r(W ∩ K) itself; it compares D_r(B) + D_r(W ∩ K) with the server's D_r(K) per author r. This catches an equivocation below B that the client cannot see op by op (6.2.3).
    • The server computes D_r(K) as D_r(head) − D_r(Δ) where Δ = V_S \ K are the ops the client lacks: O(|Δ|) per session, thanks to the incremental head sums (6.1.3).
    • A base is checked at fetch: the server sends the base with D_r(B); the client verifies it against the server's Sums for K = B in the same session, and at rebase verifies D_r(B') = D_r(B) + D_r(B' \ B) over the ops it receives or holds.
    • Mismatch → localization as 4.12, but only over W (the client has no ops below B). A mismatch that localization cannot explain within W means divergence below B: the client reports base-divergence, refuses to rebase on that base, and refetches it from scratch; if it persists, it stops with duplicate-replica-class severity (a server or base is wrong, or the client's own ops below B were equivocated: the ledger hashes decide which).
  • [decision-proposal] Digest in live sessions (Review R2). A live session runs the initial halves once and then only exchanges Ops/Committed, so without more the digest would never be rerun. Rules:
    • The server sends an unsolicited Sums frame (feature lthash) for K = the client's last Committed frontier's down-closure (exact membership) every digest_every = 10 minutes or 10 000 forwarded ops, whichever first, at every rebase (for B', before the client drops any window op), and at every reconnect (the initial halves already do it).
    • The client may also request one (Need with the marker *sums), e.g. from barocss-hub verify.
    • Cadence evasion: when a scheduled Sums is overdue (no Sums within digest_every + the 60 s step timeout), the client sends Need *sums itself; no answer within the step timeout counts as a mismatch and takes the same pause → localize → refetch → base-divergence path below. A server cannot avoid the check by staying silent.
    • Own-op check without trusting the server: for its own replica id the client computes D_self from its ledger hashes (E(h(o)) needs only h(o)), so every own op below B is checked against what the client actually authored, independent of the base.
    • On mismatch: the client pauses applying live ops (it keeps recording into the outbox), runs localization over W (4.12). If it finds the op, the usual D-008 / D-022 path applies. If not, the difference is below B: the client refetches the base from scratch and compares again; a second mismatch is a persisted base-divergence warning naming the server, live sync with that server stops (as D-022 stops sync with a duplicate replica), sync exits 3 (D-045), and the user is told to verify against another full replica (barocss-hub verify --against <remote>) or contact the server admin. Nothing local is discarded.
  • Trust limit (stated explicitly): a base, and every Sums from the server, is only as trustworthy as the server. A compromised server that lies consistently (a fake base with matching sums) is detected only for the client's own ops (ledger) and by cross-checking with another replica (another client's own-op ledger via verify, or a second full replica). Likewise the thin client's deferred TextEdit seed check (Text-CRDT 7.4.2) trusts the server's validation, so thin == full (6.10 #1) holds for #48 invalid-op verdicts only for an honest server. The periodic digest catches divergence between what the server sends over time and what the client holds, not a server that never tells the truth.
  • Underflow in LtSums::minus (observed on develop 714faf0, crates/hub-cli/src/digest.rs lines 286–295):
    • The lane subtraction Lt::sub is already wrapping_sub per u16 lane, which is the correct group subtraction mod 2^16. It needs no change.
    • The count is not modular: e.0 -= n on a u64. If t names an author missing from self (the entry(..).or_default() inserts 0) or with a larger count, it panics in debug builds and silently wraps in release builds, leaving a huge count and a nonzero entry that never compares equal. Both happen exactly when the precondition T ⊆ S is false, which a lying or diverged peer can cause (for example a Δ that is not a subset of the head). Also, an entry whose count reaches 0 is removed even if its lanes are nonzero, which hides an inconsistency.
    • Correct subtraction ([decision-proposal], fix in a later code Issue): make it fn minus(&mut self, t: &LtSums) -> Result<(), NotSubset>. First check every author of t: it must exist in self with n_t ≤ n_self (checked_sub); if any fails, return Err(NotSubset) without mutating self. Then for each author subtract lanes with wrapping_sub and the count with the (now safe) subtraction. Remove an entry only when the count is 0 and all lanes are 0; a count of 0 with nonzero lanes is Err(NotSubset) as well (it proves T ⊄ S). A caller treats NotSubset as a digest mismatch (localize), never as a panic. Tests to add: subtracting a superset, an unknown author, a same-count different-lanes set, and the round trip (S + T) − T = S.

6.8 Rescue on the server (#51)

  • [decided] (D-049) reid --rescue re-authors a copied repo's rejected edits by effect under a fresh id.
  • [decision-proposal] Under D-048, #51's option (b) server-side rescue fits best, and this spec recommends it to #51:
    • With a central server (D-038), the copy's rejected ops never enter the server's op set, so no thin client ever receives them; a third replica can only hold dependents of them through a side path (a file:// remote between two clients, or a full replica that synced with the copy directly).
    • The server runs the re-author once: the copy uploads its intended tree (the working directory at head, as 4.13), the server allocates and binds a fresh replica id to the copy's principal (6.5.4), authors the effect ops under it, and the copy rebases onto the resulting version. Every other client receives ordinary new ops: no mapping needs to be replicated.
    • Side-path dependents (full replicas only) still need #51's option (a) mapping or (c); #51 decides for that residual case.
  • [open question] for #51: whether, in online-first mode, client-to-client file:// remotes are still allowed (they are the only source of the #51 third-replica case).

6.9 Reuse vs change of develop's code, module by module

module verdict notes
hub-core::tree (op model, apply, conflicts) reuse unchanged §1–§2 unchanged; #48 adds text payloads
hub-core::version, checkpoint (versions, version ids, frontier checks) reuse causally_below needs a "below base" answer for ids in B (exact membership M(B), 6.2.2; never VV(B))
hub-core::sync (Have/Ops/Need, delta, SyncReplica, receive, pending) reuse, extend partial mode: blob-remote rule (6.2.3), receive against B ∪ W; ledger check for own ids
hub-core::tree_snapshot (#33) reuse the base encoding (6.2.2)
hub-core::projection reuse incremental per changed directory on the server (performance only)
hub-cli::wire (frames, limits, Hello) reuse, extend features live, partial, lthash; frames Ping, Pong, Base (base transfer, chunked like blobs); protocol stays 1
hub-cli::session (Side, handshake, client/server halves, compare_sums, localize, persist) reuse for the initial halves; change for live live loop after the halves; the daemon Side keeps the repo open (6.1.2); Sums over LtHash for lthash
hub-cli::digest reuse Sums; enable LtSums turn on feature lthash in the server and thin-client builds; fix minus (6.7); incremental head sums
hub-cli::sync (warnings, pending, published, sync_cmd) reuse sync on a thin client = one non-live session
hub-cli::watch change live session instead of the --sync interval; save hints; offline-limit; sparse-aware
hub-cli::wd (index, scan, diff, checkout) change sparse set: unmaterialized ≠ deleted; materialize on demand
hub-cli::blobs reuse, extend eviction with pins (6.2.5)
hub-cli::store, durable, snapshot, migrate reuse A.1/A.2 unchanged; thin-client layout marker (6.2.6)
hub-cli::remote, trust reuse, extend https:// URLs; trust key for HTTP = normalized URL + server certificate fingerprint, under D-046/D-047 rules
hub-cli::reported (D-050) reuse keyed by the trust key, works for HTTP
hub-cli::rescue (#49) reuse for full replicas server-side variant per 6.8 / #51
hub-cli::json reuse the HTTP JSON documents reuse schema 1 shapes
new: HTTP daemon, auth, API, history index new serve --http; a dependency decision (HTTP/TLS/WebSocket crates) is a separate decision-proposal in the implementation Issue
  • Migration [decision-proposal]: no change to the full-replica on-disk format; a thin client is a new layout behind a marker (6.2.6); the wire stays protocol 1 with new features, so an old full-replica client still syncs with a new server over SSH (5.10).

6.10 Testable properties [decision-proposal]

  1. Convergence over partial replicas. For any op set and any delivery order, a thin client with base B and window W reaches the same state, the same pending set and the same conflicts as a full replica holding exactly B ∪ W (with B given by its exact membership M(B), 6.2.2, including own-chain holes).
  2. Ack ⇒ durable. An op is in the server's Committed frontier only after its A.1 persist; killing the server at any crash stage after an ack never loses the op or its blob.
  3. Outbox resend. For any disconnect schedule (drops at every frame boundary), after reconnecting, every outbox op is on the server exactly once (by id) and nothing is authored twice.
  4. No id reuse. A thin client never authors two ops with one id, across restarts, rebases and evictions (the ledger is never evicted).
  5. Own-id detection. An op authored under a thin client's id that it did not author (a copied thin repo) yields a DuplicateReplica warning (D-022) at the first session, whether the op is in W or below B.
  6. Eviction is invisible to state. Evicting any evictable blob leaves state, conflicts, version id and digests unchanged; reading the file fetches identical bytes.
  7. Sparse is not delete. For any sparse set, record on a thin client never emits a Delete for a node outside the sparse set; moving a directory in or out of the set changes materialization only.
  8. Digest equality. For honest replicas, D_r(B) + D_r(W ∩ K) == D_r(K) on the server for every author r and every session; for any single equivocation below B, the check fails at the next session.
  9. Derived data = recomputation. The server's incremental state, conflicts, projection, history index and LtHash sums equal a from-scratch computation from the op log, at every ack (property test over random op streams).
  10. API equivalence. Ops submitted via POST ops with base v produce the same state as the same edits made by a thin client whose frontier was v.
  11. Idempotent API writes. Retrying POST ops with the same (principal, repo, Idempotency-Key) and body returns the same op ids and leaves the op set unchanged; a different body under the same key is 409 and leaves it unchanged; the same key from another principal is independent.
  12. Replica binding. Ops under a replica id bound to another principal (foreign-replica) or unbound (unbound-replica) are never applied on the server; enrollment never binds an id that has ops on the server.
  13. Offline limit. Past the offline limit, record refuses and leaves the working directory untouched; after reconnecting, recording resumes and no saved edit is lost.
  14. Latency. On a LAN fixture (two clients, one server on loopback with injected 2 ms RTT), save-with-hint → other client's watch event p99 ≤ 300 ms over 1000 saves (the 6.3.2 target; a bench, not a gate, until E-B7 is decided).
  15. LtSums::minus safety. For any S, T: minus returns Err(NotSubset) and leaves S unchanged iff T ⊄ S in the count sense; otherwise (S − T) + T == S; it never panics or wraps a count.
  16. Mixed fleet. A pre-#47 full-replica client syncing over SSH with a daemon-served repo behaves exactly as today (no live, no lthash); a pre-#47 serve --stdio started by an old forced command on a daemon-served repo exits with locked and writes nothing; because develop's serve waits for the lock with acquire_timeout(step_timeout), that exit comes only after up to 60 s.
  17. Periodic digest. In a live session lasting longer than digest_every, an equivocation injected below B on the server (test hook) is reported as base-divergence within one interval or at the next rebase, whichever comes first; a server that withholds the scheduled Sums and does not answer Need *sums within the step timeout is treated as a mismatch and also ends in base-divergence; an injected change to one of the client's own ops below B is detected by the ledger check.
  18. Dirty files are never dematerialized. For any incoming Move/Delete or sparse rm affecting a materialized dirty file, the file's bytes stay on disk unchanged and it is reported blocked until recorded.
  19. Authoring on the base. No authoring path (record, watch, live thin client, mutate, rescue, HTTP API) produces an op whose deps include a same-node content op outside the author's base (index base, or the API request's base); see Text-CRDT 7.12 #18.
  20. Exact base membership. For any base B with own-chain holes (e.g. (6,r) ∈ B, (5,r) ∉ B) and any op o with a dep (5,r), a thin client on B keeps o pending exactly when a full replica holding exactly B does; K, causally_below and #48 epoch membership agree with the full replica.

6.11 Worked example: 2 humans + 3 agents, one server, a short disconnect

Actors and replica ids (written as single letters; ids are (counter, replica)):

  • S server (full replica, daemon). A Alice (human, VS Code, thin client, sparse = whole tree). B Bob (human, CLI thin client, sparse = /src). G1, G2: two agents using the HTTP API; the server binds hosted replicas g1, g2 to their tokens. C a CI agent running a thin client in a container, sparse = /tests.
  • Base B0 at frontier {(10,S)}: /src/ = node (2,S), /src/a.txt = (3,S) blob x0, /src/b.txt = (4,S) blob y0, /README = (5,S) blob r0, /tests/ = (6,S). Every client holds B0 with D_r(B0) and an empty window. All are live.

Step 1: Alice saves a.txt.

  • VS Code sends saved src/a.txt; watch records (11,A) SetContent((3,S), x1) deps {(10,S)}.
  • A → S: Ops {(11,A)}, BlobBegin x1 …, Chunk, BlobEnd x1.
  • S: Lamport ok (10 < 11), blob verified, group commit, S → A: Committed {(11,A)}.
  • S → B, S → C: Ops {(11,A)}. Bob: /src is in his sparse set, so B → S: NeedBlobs [x1], S → B: the blob; Bob's a.txt is rewritten. C: /src is outside its sparse set; it applies the op with x1 remote-held (6.2.3) and fetches nothing. B → S, C → S: Committed {(11,A)}.

Step 2: Bob's Wi-Fi drops. The live socket closes; Bob's watch reports offline.

  • Bob edits b.txt → (12,B) SetContent((4,S), y1) deps {(11,A)}; mkdir docs → (13,B) Create(dir, (1,S) root, "docs") deps {(12,B)}. Both are durable in Bob's ops.log and are his outbox.

Step 3: agent G1 edits README via the API.

  • G1 → S: PUT blobs/r1, then POST ops {"base":"<id of {(11,A)}>","ops":[{"op":"set-content","path":"README","blob":"r1"}]}, Idempotency-Key: k1.
  • S authors (12,g1) SetContent((5,S), r1) deps {(11,A)}, persists, answers {"ops":["12@g1"],"version":"<id of {(12,g1)}>","conflicts":[]}.
  • S → A, S → C: Ops {(12,g1)} (Bob is offline). Alice fetches r1 (whole tree sparse); C does not. Both Committed.

Step 4: agent G2 moves b.txt into a new /lib, on the same base.

  • G2 → S: POST ops {"base":"<id of {(11,A)}>","ops":[{"op":"mkdir","path":"lib"},{"op":"move","path":"src/b.txt","to":"lib/b.txt"}]}.
  • S authors (12,g2) Create(dir, root, "lib") deps {(11,A)} and (13,g2) Move((4,S), (12,g2), "b.txt") deps {(12,g2)}. They are concurrent with (12,g1): no conflict (different nodes). Answer {"ops":["12@g2","13@g2"], …}; forwarded to A and C.

Step 5: CI agent C adds a test, then a retry.

  • C's job writes /tests/t1.rs; C's watch records (14,C) Create(file, (6,S), "t1.rs") and (15,C) SetContent(…, t1) on its index base {(12,g1),(13,g2)} (its frontier, already materialized live). C → S: Ops, blob; S → C: Committed; forwarded to A.
  • G1 retries step 3 after a timeout with Idempotency-Key: k1: S answers the same {"ops":["12@g1"], …} and authors nothing (6.10 #11).

Step 6: Alice and Bob edit b.txt concurrently.

  • Alice (online) has received (14,C),(15,C) in step 5, so her frontier is {(15,C)}. She saves lib/b.txt → (16,A) content edit of (4,S) (content y2) deps {(15,C)} = her index base (she had materialized the move, so she sees the moved file); acked and forwarded to C.
  • Bob's offline edit (12,B) on the same node is concurrent with (16,A).

Step 7: Bob reconnects after 40 s. A normal session, then live:

  • Hello ↔ Hello (features live,partial,lthash,digest,rejected-ops,…). The server checks Bob's token binds replica B.
  • Pull half: B → S: Have {(13,B)} base=B0 (with partial, Have names the client's base). The server holds neither (13,B) nor (12,B), so resolved = - and K = B0 ∪ downclose(∅) = B0, taken by its exact membership M(B0) (6.2.2); the VV hint (which names (11,A)) is not used for K. S → B: Sums with D_r(B0) per author (only S authored ops in B0). Bob checks D_S(B0) against the value shipped with his base and his W ∩ K = ∅: equal, no divergence; his own D_B from the ledger matches the server's (zero ops of B on the server yet).
  • S → B: Ops {(11,A),(12,g1),(12,g2),(13,g2),(14,C),(15,C),(16,A)} done=1 (V_S \ K; (11,A) is already in Bob's window and is skipped by id). Bob checks Lamport on ids and equivocation within W, and applies all, blobs remote-held (6.2.3). Blob needs: b.txt is now under /lib, outside Bob's sparse set /src, so y2 is not needed; README and /tests are outside too. Bob sends no NeedBlobs, then Done ops, persists, Committed {(13,B),(16,A)}.
  • Push half: S → B: Have {head}. B → S: Ops {(12,B),(13,B)}, then blob y1 (pushed inline, ≤ 1 MiB). S persists, Committed {…,(13,B)} with no R lines.
  • Meta phase, then live. S forwards (12,B),(13,B) to A and C.

Result on every replica (S, A, B, C, and the hosted replicas): op set = B0 ∪ {(11,A),(12,B),(13,B),(12,g1),(12,g2),(13,g2),(14,C),(15,C),(16,A)}, frontier {(13,B),(16,A)} ((16,A) covers (15,C), which covers (12,g1) and (13,g2)).

  • /src/a.txt = x1; /README = r1; /lib/b.txt (moved by G2, node (4,S)); /docs/ (Bob); /tests/t1.rs (C).
  • b.txt content: (12,B) and (16,A) are concurrent content edits of (4,S) (written above as content y1/y2 for brevity). b.txt is text, so under Text-CRDT §7 both are TextEdits on the epoch of y0 and merge with no conflict record (a concurrent-edit notice if they overlap, 7.8); a binary file, or a pre-#48 binary, gives today's §2.3 content conflict, identical everywhere. Bob's edit followed the move because edit and move address the NodeId. Bob's /src/b.txt was clean (recorded as (12,B)), so the incoming move out of his sparse set dematerializes it; had it been dirty, it would stay on disk as blocked (6.2.4).
  • Bob's sparse set: /src only, so he does not materialize /lib/b.txt; his status still shows the node's merge notice or conflict (state is whole-tree, 6.2.1). If Bob wants the file he runs sparse add lib.
  • Digests: at the next digest point (6.7) every client's D_r(B0) + D_r(W ∩ K) equals the server's D_r(K) per author (6.10 #8).

6.12 Escalations (user-facing choices, not decided here)

  • E-B1 [decided] D-052: HTTP authentication mechanism (6.5.4): bearer tokens, SSH-key challenge, mTLS, OIDC via proxy (proposal: tokens + SSH-key login). Supersedes the E-R3 open question (5.14), which D-048 makes required.
  • E-B2 [decided] D-052: Offline limit and behavior past it (6.4): proposal 100k ops / 1 GiB / 7 days, then refuse new records.
  • E-B3 [decided] D-052: Cache and sparse defaults (6.2.4, 6.2.5): proposal cache_max = min(2 GiB, 10 % free disk); default sparse set = whole tree.
  • E-B4 [decided] D-052: Whether full replicas remain supported for users (hydrate, today's clone) (6.4). Proposal: yes, opt-in.
  • E-B5 [decided] D-052: Exactly one server per repo, or promotable/failover servers (6.1.1).
  • E-B6 [decided] D-052: Virtual checkout in the alpha: VS Code FileSystemProvider, OS virtual filesystems, or none (6.2.4).
  • E-B7 [decided] D-052: Latency target numbers (6.3.2): proposal p50 ≤ 150 ms / p99 ≤ 300 ms on LAN.
  • E-B8 [decided] D-052: Whether the HTTP API may write without a base ("on head", last-writer semantics); proposal: no (6.5.3).
  • E-B9 [decided] D-052: Identity in history: the replica → principal binding makes "who" visible on the server; D-006 and D-010 set a private-by-default stance. Should API history answers show principal names, and to whom?

6.13 Open questions

  • For #48: (1) the unit a thin client fetches for a text file (sequence state at B plus W ops) and its size bound; (2) whether text ops of a file outside the sparse set are kept in W as bodies or only digested; (3) server-side diff of whole-file set-content against the text at base, deterministic across server versions; (4) line blame from the text CRDT; (5) tombstone GC at base cuts (a base is a natural compaction point, but a window op may still reference a tombstone); (6) how a content conflict recorded before the upgrade is presented once text merge exists.
  • For #51: client-to-client file:// remotes under online-first (6.8).
  • Bases: only at checkpoints or also automatic (6.2.2).
  • Server warnings visible to clients (5.8), now per principal.
  • Persisting the server's incremental LtHash sums (a format change under the #33 bar; D-051 says not now).
  • Whether serve --stdio sessions should be proxied to the daemon or refused while a daemon runs (6.1.2).
  • HTTP/TLS/WebSocket dependency choice (implementation Issue).

6.14 Impact on queued Issues (re-scoping proposals)

  • #44 background service (D-039, held by D-048): now "keep each registered thin client connected": one live session per repo with reconnect backoff, outbox size and offline-limit in service status --json, save hints from VS Code routed to the service's watcher. Launchd/systemd parts unchanged. A server-side unit (serve --http daemon) is a separate item, not #44.
  • #36 install: add the server artifact and a sample systemd unit for serve --http with TLS; client packages unchanged. clone --thin becomes the documented default in the install slide once implemented.
  • #40 docs: Concepts gain "online-first and thin clients", sparse sets, the offline limit, the API for agents; the command reference gains clone --thin, sparse, hydrate, auth, serve --http; "Coming from git" says there is no merge and no full clone by default. Write after the implementation Issues, not before.
  • #41 VS Code follow-ups: items 1–5 stay. Add: send saved <path> hints (6.3); show connection state, outbox size and offline-limit; handle offline-not-cached on open; optionally a FileSystemProvider for virtual checkout (E-B6). Item 4's wording ("Keep mine" makes a sequential edit) stays true under direction B.

Clone this wiki locally