Repository navigation
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.
- [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).
- [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
ReplicaIdand 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, withRlines 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-onlystays; under 6.5 it becomes a per-principal scope.
-
[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 --stdioreopens 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 --stdiosessions 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 localrecordon 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).
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.
-
[decision-proposal] A thin client holds:
- a base
B: a server-provided snapshot at versionv_B(6.2.2); - a window
W: the full bodies of every op it received or authored aboveB(B ∪ Wis down-closed, as every version is); - its own-op ledger (6.2.3);
- its outbox: own ops not yet covered by a server ack (6.4);
- a blob cache, bounded (6.2.5), plus the working directory of its sparse set;
- local config, trust store (D-046), warnings, reported rejections (D-050).
- a base
-
[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.
-
[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 thetree_snapshotencoding (#33,Replica::encode_snapshot) plus: the frontier, the exact membershipM(B)(below), the per-authorLtSumsD_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
Bcan hold(6,r)without(5,r). Deciding "below B" byVV(B)would let a thin client apply an op that depends on(5,r)while a full replica holding exactlyBkeeps it pending, breaking 6.10 #1. So a base shipsM(B): for each authorr, the exact set of counterscwith(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 inBcosts ranges only where its Lamport counters jump.-
id ∈ B⇔id.counteris inr's range list. This exact test, neverVV, is used for causal readiness, forK(6.7), forcausally_belowon 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 inV=B ∪ Wat the thin client). - At rebase,
M(B')replacesM(B); ids ofWthat are inM(B')are dropped from the window.
-
-
[decision-proposal] Rebase: when a newer base
B'withB ⊆ B' ⊆ B ∪ W ∪ (ops the client lacks)exists, the client fetches it, checksD_r(B') == D_r(B) + D_r(W ∩ B') + D_r(fetched ops)for every author (6.7), dropsW ∩ B'from the window, and keepsW \ B'. A client never rebases over an outbox op the server has not acked (by constructionB'only contains server-durable ops, so an unacked op is never inB'). - [open question] Whether bases are cut only at published checkpoints (fewer, user-visible) or also automatically.
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'sCommittedfrontier 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.
-
[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 withNeedBlobs(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 bystatus/status --json(blocked, reasondirty-outside-sparse) and a watch event.recordrecords 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 rmrefuses 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
recordnever sees those paths, so it never emits Deletes for them (thewdscan must treat "not materialized" as "absent by choice", not "deleted"; this is the onewdchange thin clients need). -
[decision-proposal] Virtual checkout (materialize on open) is optional and per integration: the VS Code extension's
FileSystemProviderfirst (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.
-
[decision-proposal] The cache is
.hub/blobs/on a thin client, with a byte budgetcache_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 liveTextDocis 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.
-
[decision-proposal]
.hub/with a markerpartial(like the #49v1-rescuereplica marker),base/<version-id>(snapshot),ops.logholdingWonly (format v1 batches),ledger(own ops),outboxis not a separate file (it is the own ops ofWnot covered byserver-acked),server-acked(the lastCommittedfrontier),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).
- 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). - The watcher records immediately on a hint (no debounce) and after the debounce otherwise (400 ms today).
recordproduces ops whose deps are the index base (#25, as develop'sprepare_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 remoteSetContentstays a visiblecontentloser 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. - The live session (feature
live) sends an unsolicitedOpsframe with the new ops, followed byBlobBegin…BlobEndfor new blobs up toinline_blob= 1 MiB (pushing them avoids aNeedBlobsround trip). Larger blobs wait for the server'sNeedBlobs. - The server validates (§4.5), group-commits (6.1.2) and replies
Committed <frontier>(plusRlines). The client records the frontier inserver-acked; the outbox shrinks. - The server forwards the ops in one
Opsframe 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
Opsat any time after the initial pull and push halves; eachOpsbatch from a client is answered byCommitted, and each forwarded batch to a client is answered by the client'sCommitted, so the server knows what each client holds (noHaveper edit). APing/Pongframe pair (new types, featurelive) 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 outstandingCommittedonly. -
[decision-proposal] Live mode replaces the
--sync2 s polling interval ofwatchfor remotes that announcelive; others keep polling.
-
[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
recordcosts 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).
- 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
-
[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 +
--sync2 s); this target needs live mode, the hint, and the daemon (6.1.2).
- [decided] (D-048) Ops are buffered and resent; convergence is unchanged.
-
[decision-proposal] While disconnected,
recordkeeps 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 inops.log). Reconnect = a normal session:Hello, pull half, push half (5.2.4), then live. Resend is exactly today's push half: the server answersHavewithV_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 aswatchdoes 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 belowB; sparse-set changes that need new blobs. Everything else (record, status, log ofW, 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, andoffline_max_age= 7 days. Past any limit, a thin client refuses new records (watch stops recording and reportsoffline-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 hydrateturns a thin client into a full replica while online (fetch all ops belowBand 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.
- 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:
- [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.
- [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
--rootlike 5.9.2; no repo listing unless the principal is allowed.
| 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, orhead. Every answer carries the version id it was computed at, so an agent can write against it.
-
[decision-proposal]
POST opswith{"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":…} ]}, headerIdempotency-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 is409 idempotency-conflictand authors nothing. Blobs are uploaded first withPUT 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 versionvand writes "onv" gets exactly the concurrency semantics a human client gets: its edit is concurrent with everything recorded afterv, and §2/§3 converge it (move vs delete per D-001, revive per D-023, content conflicts per §2.3 until #48).baseis required; a missingbaseis400 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 atbase(a collisionname~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 sameIdempotency-Keyreturns 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.
- [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 scopesread,write,adminper repo;serve --read-only(D-043) caps every principal atread. -
[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
PcallsPOST enroll(CLI: done byclone --thin,remote addagainst an HTTP remote, orbarocss-hub auth enroll). The server returns a fresh random replica id (128-bit, never used) bound toP; 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 doesreidsemantics: 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 withunbound-replica(D-008 warning, exit 3) until an admin binds it:barocss-hub serve admin bind <replica> <principal>(or a mapping filereplica-bindingsunder 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.
-
Enrollment of a new replica: an authenticated principal
-
[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 --stdioconnects 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/lockfor its lifetime, so a pre-#47 binary in an old forced command (which opens the repo itself) getslockedand writes nothing behind the daemon's back. - Options for the mechanism ([decided] (E-B1, D-052), the human chooses):
-
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. -
SSH-key challenge → short-lived token:
barocss-hub auth loginsigns a server challenge with the user's SSH key (ssh-agent,ssh-keygen -Y sign), the server checks it againstauthorized_keys-style entries and returns a token valid for hours. Reuses the SSH trust users already have; agents still need option 1. - mTLS client certificates: strong, no bearer secret on the wire; certificate issuance is operational work.
- 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.
-
Bearer tokens over TLS: server-issued, scoped, revocable personal and agent tokens (
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
Payloadvariant(s) for text edits (or a per-file sub-log; #48 decides), authored with ordinaryOpIds and deps, so they flow throughOpsframes, 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):
recordcallsdifffor a text file instead of emittingSetContent. -
Server derived data (6.1.3): a per-node
TextStateat 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
TextStateatBplus the text ops inW, because a client must hold the sequence state of a file to author a mergeable edit on it. -
API (6.5.3):
set-contenton a text file is diffed by the server against the file's text atbase, so two agents writing whole files on the same base merge instead of conflicting. - Open questions handed to #48 are listed in 6.13.
-
[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 theSumsframe (5.2.8) the per-author checksum is the 32-byte BLAKE3 checksum of the LtHash16 valueD_r(K)instead ofS_r(K).- With
partial,Havealso names the client's base, andK = B ∪ downclose(resolved), whereBis taken by its exact membershipM(B)(6.2.2); theVVhint is never used to computeK(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 computesD_r(W ∩ K)itself; it comparesD_r(B) + D_r(W ∩ K)with the server'sD_r(K)per authorr. This catches an equivocation belowBthat the client cannot see op by op (6.2.3). - The server computes
D_r(K)asD_r(head) − D_r(Δ)whereΔ = V_S \ Kare 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'sSumsforK = Bin the same session, and at rebase verifiesD_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 belowB). A mismatch that localization cannot explain withinWmeans divergence belowB: the client reportsbase-divergence, refuses to rebase on that base, and refetches it from scratch; if it persists, it stops withduplicate-replica-class severity (a server or base is wrong, or the client's own ops belowBwere equivocated: the ledger hashes decide which).
- With
-
[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
Sumsframe (featurelthash) forK= the client's lastCommittedfrontier's down-closure (exact membership) everydigest_every= 10 minutes or 10 000 forwarded ops, whichever first, at every rebase (forB', before the client drops any window op), and at every reconnect (the initial halves already do it). - The client may also request one (
Needwith the marker*sums), e.g. frombarocss-hub verify. -
Cadence evasion: when a scheduled
Sumsis overdue (noSumswithindigest_every+ the 60 s step timeout), the client sendsNeed *sumsitself; no answer within the step timeout counts as a mismatch and takes the same pause → localize → refetch →base-divergencepath 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_selffrom its ledger hashes (E(h(o))needs onlyh(o)), so every own op belowBis 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 belowB: the client refetches the base from scratch and compares again; a second mismatch is a persistedbase-divergencewarning naming the server, live sync with that server stops (as D-022 stops sync with a duplicate replica),syncexits 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.
- The server sends an unsolicited
-
Trust limit (stated explicitly): a base, and every
Sumsfrom 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 viaverify, or a second full replica). Likewise the thin client's deferredTextEditseed check (Text-CRDT 7.4.2) trusts the server's validation, so thin == full (6.10 #1) holds for #48invalid-opverdicts 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 develop714faf0,crates/hub-cli/src/digest.rslines 286–295):- The lane subtraction
Lt::subis alreadywrapping_subper u16 lane, which is the correct group subtraction mod 2^16. It needs no change. - The count is not modular:
e.0 -= non au64. Iftnames an author missing fromself(theentry(..).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 preconditionT ⊆ Sis 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 oft: it must exist inselfwithn_t ≤ n_self(checked_sub); if any fails, returnErr(NotSubset)without mutatingself. Then for each author subtract lanes withwrapping_suband 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 isErr(NotSubset)as well (it provesT ⊄ S). A caller treatsNotSubsetas 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.
- The lane subtraction
-
[decided] (D-049)
reid --rescuere-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.
- 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
-
[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).
| 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).
-
Convergence over partial replicas. For any op set and any delivery order, a thin client with base
Band windowWreaches the same state, the same pending set and the same conflicts as a full replica holding exactlyB ∪ W(withBgiven by its exact membershipM(B), 6.2.2, including own-chain holes). -
Ack ⇒ durable. An op is in the server's
Committedfrontier only after its A.1 persist; killing the server at any crash stage after an ack never loses the op or its blob. - 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.
- No id reuse. A thin client never authors two ops with one id, across restarts, rebases and evictions (the ledger is never evicted).
-
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
Wor belowB. - Eviction is invisible to state. Evicting any evictable blob leaves state, conflicts, version id and digests unchanged; reading the file fetches identical bytes.
-
Sparse is not delete. For any sparse set,
recordon 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. -
Digest equality. For honest replicas,
D_r(B) + D_r(W ∩ K) == D_r(K)on the server for every authorrand every session; for any single equivocation belowB, the check fails at the next session. - 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).
-
API equivalence. Ops submitted via
POST opswith basevproduce the same state as the same edits made by a thin client whose frontier wasv. -
Idempotent API writes. Retrying
POST opswith 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. -
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. -
Offline limit. Past the offline limit,
recordrefuses and leaves the working directory untouched; after reconnecting, recording resumes and no saved edit is lost. - 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).
-
LtSums::minussafety. For anyS,T:minusreturnsErr(NotSubset)and leavesSunchanged iffT ⊄ Sin the count sense; otherwise(S − T) + T == S; it never panics or wraps a count. -
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 --stdiostarted by an old forced command on a daemon-served repo exits withlockedand writes nothing; because develop'sservewaits for the lock withacquire_timeout(step_timeout), that exit comes only after up to 60 s. -
Periodic digest. In a live session lasting longer than
digest_every, an equivocation injected belowBon the server (test hook) is reported asbase-divergencewithin one interval or at the next rebase, whichever comes first; a server that withholds the scheduledSumsand does not answerNeed *sumswithin the step timeout is treated as a mismatch and also ends inbase-divergence; an injected change to one of the client's own ops belowBis detected by the ledger check. -
Dirty files are never dematerialized. For any incoming Move/Delete or
sparse rmaffecting a materialized dirty file, the file's bytes stay on disk unchanged and it is reported blocked until recorded. -
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. -
Exact base membership. For any base
Bwith own-chain holes (e.g.(6,r) ∈ B,(5,r) ∉ B) and any opowith a dep(5,r), a thin client onBkeepsopending exactly when a full replica holding exactlyBdoes;K,causally_belowand #48 epoch membership agree with the full replica.
Actors and replica ids (written as single letters; ids are (counter, replica)):
-
Sserver (full replica, daemon).AAlice (human, VS Code, thin client, sparse = whole tree).BBob (human, CLI thin client, sparse =/src).G1,G2: two agents using the HTTP API; the server binds hosted replicasg1,g2to their tokens.Ca CI agent running a thin client in a container, sparse =/tests. - Base
B0at frontier{(10,S)}:/src/= node(2,S),/src/a.txt=(3,S)blobx0,/src/b.txt=(4,S)bloby0,/README=(5,S)blobr0,/tests/=(6,S). Every client holdsB0withD_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:/srcis in his sparse set, soB → S:NeedBlobs [x1],S → B: the blob; Bob'sa.txtis rewritten.C:/srcis outside its sparse set; it applies the op withx1remote-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'sops.logand are his outbox.
Step 3: agent G1 edits README via the API.
-
G1 → S:PUT blobs/r1, thenPOST ops {"base":"<id of {(11,A)}>","ops":[{"op":"set-content","path":"README","blob":"r1"}]},Idempotency-Key: k1. -
Sauthors(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 fetchesr1(whole tree sparse); C does not. BothCommitted.
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"}]}. -
Sauthors(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:Sanswers 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 saveslib/b.txt→(16,A)content edit of(4,S)(contenty2) 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(featureslive,partial,lthash,digest,rejected-ops,…). The server checks Bob's token binds replicaB. - Pull half:
B → S:Have {(13,B)} base=B0(withpartial,Havenames the client's base). The server holds neither(13,B)nor(12,B), so resolved =-andK = B0 ∪ downclose(∅)=B0, taken by its exact membershipM(B0)(6.2.2); theVVhint (which names(11,A)) is not used forK.S → B:SumswithD_r(B0)per author (onlySauthored ops inB0). Bob checksD_S(B0)against the value shipped with his base and hisW ∩ K = ∅: equal, no divergence; his ownD_Bfrom the ledger matches the server's (zero ops ofBon 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 withinW, and applies all, blobs remote-held (6.2.3). Blob needs:b.txtis now under/lib, outside Bob's sparse set/src, soy2is not needed;READMEand/testsare outside too. Bob sends noNeedBlobs, thenDone ops, persists,Committed {(13,B),(16,A)}. - Push half:
S → B:Have {head}.B → S:Ops {(12,B),(13,B)}, then bloby1(pushed inline, ≤ 1 MiB).Spersists,Committed {…,(13,B)}with noRlines. - Meta phase, then live.
Sforwards(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.txtcontent:(12,B)and(16,A)are concurrent content edits of(4,S)(written above as contenty1/y2for brevity).b.txtis text, so under Text-CRDT §7 both areTextEdits on the epoch ofy0and merge with no conflict record (aconcurrent-editnotice if they overlap, 7.8); a binary file, or a pre-#48 binary, gives today's §2.3contentconflict, identical everywhere. Bob's edit followed the move because edit and move address the NodeId. Bob's/src/b.txtwas 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:
/srconly, so he does not materialize/lib/b.txt; hisstatusstill shows the node's merge notice or conflict (state is whole-tree, 6.2.1). If Bob wants the file he runssparse add lib. - Digests: at the next digest point (6.7) every client's
D_r(B0) + D_r(W ∩ K)equals the server'sD_r(K)per author (6.10 #8).
- 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?
- For #48: (1) the unit a thin client fetches for a text file (sequence state at
BplusWops) and its size bound; (2) whether text ops of a file outside the sparse set are kept inWas bodies or only digested; (3) server-side diff of whole-fileset-contentagainst the text atbase, 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 acontentconflict 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 --stdiosessions should be proxied to the daemon or refused while a daemon runs (6.1.2). - HTTP/TLS/WebSocket dependency choice (implementation Issue).
-
#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-limitinservice status --json, save hints from VS Code routed to the service's watcher. Launchd/systemd parts unchanged. A server-side unit (serve --httpdaemon) is a separate item, not #44. -
#36 install: add the server artifact and a sample systemd unit for
serve --httpwith TLS; client packages unchanged.clone --thinbecomes 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 andoffline-limit; handleoffline-not-cachedon open; optionally aFileSystemProviderfor virtual checkout (E-B6). Item 4's wording ("Keep mine" makes a sequential edit) stays true under direction B.