* fix(daemon): avoid socket cancel on stop Signal active sockets with shutdown only so pending I/O wakes without entering Boost.Asio's reactor cancel path during teardown. * fix(metadata): open read pools read-only * ci: fix coverage report generation * fix(metadata): tag unscoped pool holders * test(daemon): avoid ipc teardown takeover * ci: skip plugin coverage objects * fix(daemon): repair completed FTS5 ghosts * build(vector): guard unusable FAISS headers * ci: use clang gcov for coverage * test(daemon): stabilize arm CI flakes * Harden search and compression numeric config * fix(daemon): reconcile orphan vector embeddings * fix: address vector cleanup review comments * chore(main): release 0.16.1 * Harden search ranking divisors * Harden snippet distance and command registration * test(storage): add readiness coverage gate Document the distributed sync readiness surface and add direct adapter, refcount, and GC failure-path coverage. * test(storage): cover strict resolver fallback * fix(search): tighten retrieval ranking Honor component weights in default RRF, over-fetch vector chunks before document aggregation, and persist topology centroids for sparse-guided routing. * fix(search): route weak queries through topology * fix(search): improve rerank and weak queries Wire provider-backed cross reranking, add direct Simeon candidates for weak lexical pools, and report stage-level retrieval metrics. * fix(storage): honor explicit plugin names Add readiness coverage for factory, URL backend, and compressed storage failure paths. * fix(storage): avoid remote size listing * fix(api): rollback failed content adds * test(storage): cover engine readiness * test(storage): cover compressed readiness * fix(core): harden delete and recovery paths Route prune and local delete fallback through DocumentService so content, metadata, vectors, and KG cleanup stay consistent. Treat transient SQLite quick_check lock failures as ResourceBusy, refresh IPC activity on streaming writes, and preserve reranker config in tuner seeding. * test(storage): cover URL backend readiness * fix(api): harden content rollback * feat(storage): add list seam, URL listing, and write-edge coverage * feat(api): reconcile ambiguous remote manifest writes * feat(storage): add deep compressed scan validation * test(storage): add plugin loader error path coverage * fix(gc): prune reference rows after collection * test(api,storage): add health, compact, gc, and verify edge coverage * docs: update storage-readiness coverage to 71.8% * test(metadata): add connection pool shutdown-while-leased coverage * test(metadata): add tree builder snapshot root stability coverage * test(vector): add before-init and after-close operation rejection coverage * test(metadata): add database post-close, open-failure, and cache-clear coverage * test(integrity): add P2P wrong-hash rejection + batch matrix resolution * docs: audit and resolve all matrix rows — 18 fully covered * test(metadata): add failed-migration and rollback coverage * test(storage): add getBlockSize-invalid-key, empty-verify, PUT/DELETE retry coverage * feat(storage): add file-open and rename failure hooks + ContentStore retrieve edge coverage * docs: add broad-surface coverage with per-subsystem breakdown * test(integrity): add chunk validator manifest validation coverage * test(vector): add batch insert, delete, and update operation coverage * docs: update integrity subsystem coverage to 46.4% * docs: consolidate coverage report — clean matrix, remove boilerplate * docs: update coverage with complete-suite numbers — 59.8% broad, 74.3% narrow * test(storage): add URLBackend HEAD exists with 2xx coverage * bump submodules * fix: address PR review — curl_slist leak, ec ordering, CAS rollback, production hooks --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
283 lines
16 KiB
Markdown
283 lines
16 KiB
Markdown
# Distributed Corpus and P2P Sync Protocol
|
|
|
|
Status: draft design for the v0.x roadmap. This document describes the target
|
|
shape of distributed YAMS corpora; it is not an implementation contract yet.
|
|
|
|
## Purpose
|
|
|
|
YAMS is local-first: the current corpus is a content-addressed block store plus
|
|
local SQLite-backed catalogs, FTS, vector state, and Knowledge Graph state. A
|
|
distributed corpus must let several machines or containers share the same logical
|
|
corpus without sharing a live SQLite database or assuming that object-store list
|
|
operations are complete, cheap, or strongly consistent.
|
|
|
|
The design splits the system into two planes:
|
|
|
|
| Plane | Data | Rule |
|
|
|-------|------|------|
|
|
| Object plane | CAS blocks, chunk manifests, signed checkpoints | Immutable by hash; verify before use |
|
|
| Catalog plane | document metadata, paths, tags, tombstones, KG edges, vector cache manifests | Replicated by append-only ops and checkpoint roots |
|
|
|
|
## Current Implementation Map
|
|
|
|
| Area | Current source | Distributed implication |
|
|
|------|----------------|-------------------------|
|
|
| CAS blocks | `include/yams/storage/storage_engine.h`, `src/storage/storage_engine.cpp` | Hash-keyed blocks are a good replication unit, but local filesystem layout is not itself a sync protocol. |
|
|
| Content store | `include/yams/api/content_store.h`, `src/api/content_store_impl.cpp` | Document add/get/delete must publish catalog ops in addition to writing chunks. |
|
|
| Remote backends | `include/yams/storage/storage_backend.h`, `src/storage/storage_backend_factory.cpp`, `src/storage/storage_backend_engine_adapter.cpp` | S3/R2 support can host immutable objects, but list-heavy reconciliation and size scans do not scale. |
|
|
| Object plugins | `include/yams/plugins/object_storage_v1.h`, `include/yams/plugins/object_storage_iface.hpp` | Provider health, checksum, versioning, and DR hooks should feed sync readiness. |
|
|
| Metadata catalog | `include/yams/metadata/metadata_repository.h`, `src/metadata/metadata_repository.cpp` | SQLite remains node-local; replicated catalog state must be imported through repository APIs. |
|
|
| Knowledge Graph | `include/yams/metadata/knowledge_graph_store.h`, `src/metadata/knowledge_graph_store_sqlite.cpp` | KG mutations need deterministic op IDs and conflict rules. |
|
|
| Vector state | `include/yams/vector/vector_database.h`, `src/vector/vector_database.cpp`, `src/vector/sqlite_vec_backend.cpp` | Embeddings are derived from content hash and model ID; they may be synced as cache artifacts or rebuilt. |
|
|
| P2P repair hook | `include/yams/integrity/repair_manager.h`, `src/integrity/repair_manager.cpp` | `RepairStrategy::FromP2P` only fetches missing blocks today; it is not corpus sync. |
|
|
| Daemon readiness | `src/daemon/components/ServiceManager.cpp`, `include/yams/daemon/components/StateComponent.h` | Sync must only run when content store, database, metadata repo, and vector readiness gates are explicit. |
|
|
|
|
## Non-Goals
|
|
|
|
- Do not share the live SQLite metadata database across machines.
|
|
- Do not require a central coordinator for local-first operation.
|
|
- Do not treat object-store `list()` as the source of truth.
|
|
- Do not make vector indexes authoritative. They are reproducible caches unless explicitly checkpointed.
|
|
- Do not implement Byzantine consensus in v0.x. Verify hashes and signatures, but target trusted personal or team peers first.
|
|
|
|
## Corpus Identity
|
|
|
|
Each distributed corpus has stable identity and per-device writers.
|
|
|
|
| Field | Meaning |
|
|
|-------|---------|
|
|
| `corpus_id` | Random 128-bit or 256-bit identifier created at corpus initialization. |
|
|
| `site_id` | Stable identifier for one machine, container, or daemon instance. |
|
|
| `writer_key` | Signing key used to sign ops and checkpoints from one site. |
|
|
| `epoch` | Monotonic generation for destructive administrative events such as restore or re-key. |
|
|
| `checkpoint_hash` | Hash of the current signed checkpoint manifest. |
|
|
|
|
Peers trust a corpus only after an explicit join action imports the `corpus_id`,
|
|
writer public keys, and current checkpoint root. Object-store credentials do not
|
|
imply catalog trust.
|
|
|
|
## Object Namespace
|
|
|
|
Use a namespace that avoids global listing and makes pointer reads bounded.
|
|
|
|
| Prefix | Contents |
|
|
|--------|----------|
|
|
| `blocks/sha256/<aa>/<hash>` | CAS block bytes, optionally compressed by the existing content store path. |
|
|
| `manifests/chunks/<hash>.json` | Document chunk manifest keyed by manifest hash. |
|
|
| `ops/<site_id>/<seq>.json` | Signed catalog operation. |
|
|
| `checkpoints/<checkpoint_hash>.json` | Signed checkpoint manifest. |
|
|
| `heads/<site_id>.json` | Latest signed head for one writer. |
|
|
| `corpus/head.json` | Optional single-writer or compare-and-swap pointer for shared-store mode. |
|
|
|
|
The protocol reconciles from `heads/<site_id>.json` and checkpoint references.
|
|
Listing `blocks/` is never required for normal sync.
|
|
|
|
## Checkpoint Manifest
|
|
|
|
A checkpoint is a signed, content-addressed summary of catalog and object roots.
|
|
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"corpus_id": "...",
|
|
"epoch": 1,
|
|
"site_id": "macbook-a",
|
|
"sequence": 1842,
|
|
"previous": ["sha256:..."],
|
|
"created_at": "2026-05-24T00:00:00Z",
|
|
"object_roots": ["sha256:..."],
|
|
"catalog_op_ranges": [{"site_id": "macbook-a", "from": 1, "to": 1842}],
|
|
"tombstone_root": "sha256:...",
|
|
"kg_root": "sha256:...",
|
|
"vector_roots": [{"model_id": "all-MiniLM-L6-v2", "root": "sha256:..."}],
|
|
"signature": "..."
|
|
}
|
|
```
|
|
|
|
The checkpoint must be small enough to fetch on every sync round. Large catalogs
|
|
are represented by Merkleized op ranges, not one giant document list.
|
|
|
|
## Catalog Operation Model
|
|
|
|
Every user-visible mutation produces an operation with a deterministic ID.
|
|
|
|
| Operation | Required fields | Merge rule |
|
|
|-----------|-----------------|------------|
|
|
| `document.added` | document ID, content hash, chunk manifest hash, path, metadata | Add if content and manifest verify. Concurrent same-content adds merge aliases. |
|
|
| `document.removed` | document ID or path, tombstone ID, causal context | Tombstone wins over older updates. Concurrent remove/update becomes a conflict record. |
|
|
| `document.path_set` | document ID, old path, new path, causal context | Last causal writer wins when ordered; concurrent path changes are kept as aliases until resolved. |
|
|
| `tags.added` | document ID, tag set | Observed-remove set. Additions commute. |
|
|
| `tags.removed` | document ID, tag set, observed add IDs | Removes only observed additions. Concurrent unseen adds survive. |
|
|
| `metadata.set` | document ID, key, value, causal context | Register by key. Concurrent different values create a conflict value. |
|
|
| `kg.edge_added` | source, relation, target, provenance op | OR-set edge keyed by provenance. |
|
|
| `kg.edge_removed` | source, relation, target, observed edge IDs | Removes only observed edges. |
|
|
| `vector.cache_advertised` | content hash, chunk ID, model ID, vector root | Cache hint only; never authorizes document presence. |
|
|
|
|
Deletes use tombstones with retention. Garbage collection cannot remove blocks
|
|
reachable from any retained checkpoint or unresolved tombstone horizon.
|
|
|
|
## Conflict Rules
|
|
|
|
The first implementation should prefer deterministic, inspectable outcomes over
|
|
silent data loss.
|
|
|
|
| Data | Rule |
|
|
|------|------|
|
|
| CAS blocks | No conflict. Hash mismatch rejects the block. |
|
|
| Chunk manifests | No conflict by hash. Different manifests for the same path create document-version conflict metadata. |
|
|
| Paths | Causally ordered updates use the newest op. Concurrent updates preserve both as aliases and surface a conflict. |
|
|
| Tags | OR-set semantics. Concurrent add/remove does not drop unseen adds. |
|
|
| Scalar metadata | Last causal writer wins only when causally ordered. Concurrent different values are surfaced. |
|
|
| Deletes | Tombstone beats causally older updates. Concurrent delete/update is visible until user or policy resolves it. |
|
|
| KG edges | OR-set semantics with provenance IDs. |
|
|
| Vectors | Rebuild or accept only when `(content_hash, chunk_id, model_id, model_version)` matches. |
|
|
|
|
This follows the same broad lessons as Bayou-style application conflict handling,
|
|
Dynamo-style versioned reconciliation, and CRDT set/register designs: make
|
|
causality explicit and do not hide concurrent writes behind a blind timestamp.
|
|
|
|
## Sync Round
|
|
|
|
The daemon-to-daemon protocol can be implemented over the existing daemon IPC
|
|
transport family later. The shared-object-store mode uses the same messages as
|
|
logical operations.
|
|
|
|
| Step | Message | Purpose |
|
|
|------|---------|---------|
|
|
| 1 | `hello` | Exchange protocol version, corpus ID, site ID, public key, capabilities. |
|
|
| 2 | `heads` | Advertise known site heads and checkpoint hashes. |
|
|
| 3 | `want` | Request missing checkpoints, op ranges, manifests, or block hashes. |
|
|
| 4 | `have` | Return availability, sizes, checksums, and optional provider locations. |
|
|
| 5 | `fetch` | Transfer immutable objects or catalog ops. |
|
|
| 6 | `verify` | Hash and signature verification before import. |
|
|
| 7 | `apply` | Apply catalog ops through repository APIs inside local transactions. |
|
|
| 8 | `ack` | Publish new local head after import and derived index updates are durable. |
|
|
|
|
Partial failure is safe because objects are immutable and catalog ops are
|
|
idempotent by operation ID.
|
|
|
|
## Shared Object Store Mode
|
|
|
|
This is the safest first phase for multiple machines or containers.
|
|
|
|
| Constraint | Required behavior |
|
|
|------------|-------------------|
|
|
| Single writer | Only one site may update `corpus/head.json`; other sites read and repair. |
|
|
| Multi-writer | Each site writes only `heads/<site_id>.json`; readers merge heads locally. |
|
|
| Pointer updates | Use provider versioning or compare-and-swap where available. Without CAS, use per-site heads only. |
|
|
| Listing | Do not rely on full bucket listing. Fetch from known heads and manifests. |
|
|
| Credentials | Prefer read-only credentials for readers and prefix-scoped writer credentials for writers. |
|
|
| Fallback | If remote bootstrap falls back local, the daemon must mark sync degraded rather than silently forking the corpus. |
|
|
|
|
The current R2 benchmark shows non-zero failure rates under multi-client load.
|
|
Shared-store sync must therefore treat remote writes as retryable and idempotent,
|
|
and it must verify object presence before publishing a head.
|
|
|
|
## P2P Repair Mode
|
|
|
|
The existing `RepairStrategy::FromP2P` should become the block-transfer backend
|
|
for missing CAS data, not the catalog authority.
|
|
|
|
Required behavior:
|
|
|
|
| Requirement | Reason |
|
|
|-------------|--------|
|
|
| Fetch by hash only | Prevent path traversal and confused-deputy reads. |
|
|
| Verify SHA-256 before store | Matches current `RepairManager::storeIfValid` behavior. |
|
|
| Attribute repair source | Needed for audit logs and peer health. |
|
|
| Refuse unknown corpus IDs | Prevent cross-corpus block injection. |
|
|
| Rate-limit and bound block size | Prevent peer resource exhaustion. |
|
|
|
|
## Derived Indexes
|
|
|
|
FTS, vector indexes, and KG projections should be reproducible from catalog ops
|
|
and source content. The import pipeline should apply state in this order:
|
|
|
|
1. Verify and store CAS blocks.
|
|
2. Import chunk manifests.
|
|
3. Apply metadata ops and tombstones.
|
|
4. Rebuild or import FTS state.
|
|
5. Rebuild or import KG projections.
|
|
6. Rebuild or import vector caches only when model identity matches.
|
|
7. Publish local head after durable catalog and readiness checks pass.
|
|
|
|
If vector import fails, document sync can still succeed with `vectorDbReady=false`
|
|
or `vectorIndexReady=false`. Search quality degrades, but corpus correctness does
|
|
not.
|
|
|
|
## Security Model
|
|
|
|
| Control | Requirement |
|
|
|---------|-------------|
|
|
| Manifest signing | Every op, head, and checkpoint is signed by a trusted writer key. |
|
|
| Hash verification | Every block and manifest is verified before storing or applying. |
|
|
| Peer authentication | Daemon-to-daemon mode uses mutually authenticated peers. |
|
|
| Credential scope | Object-store credentials are project-scoped and prefix-scoped where possible. |
|
|
| Secret handling | Credentials never appear in catalog ops, checkpoints, or logs. |
|
|
| Encryption | Transport encryption is mandatory for P2P. Object-store encryption follows provider policy plus optional client-side encryption. |
|
|
| Revocation | Removing a writer key starts a new epoch and requires a checkpoint signed by an administrator key. |
|
|
|
|
## Readiness Gates
|
|
|
|
Sync must be disabled or degraded unless these daemon states are explicit:
|
|
|
|
| Gate | Required state |
|
|
|------|----------------|
|
|
| Content store | Initialized with the intended local or remote storage engine. |
|
|
| Database | Open, migrated, and accepting transactions. |
|
|
| Metadata repository | Ready and bound to the active connection pool. |
|
|
| KG store | Ready before applying KG ops; otherwise queue KG ops after metadata import. |
|
|
| Vector DB | Ready before importing vector caches; not required for document metadata import. |
|
|
| Object backend | Healthy, checksum-capable, and not silently in fallback-local mode for shared-store sync. |
|
|
| Repair service | Running only when required storage/catalog dependencies are ready. |
|
|
|
|
The test gate for these classes is tracked in
|
|
`docs/testing/coverage_report.md`.
|
|
|
|
## Observability
|
|
|
|
Expose these metrics before enabling multi-writer sync by default:
|
|
|
|
| Metric | Meaning |
|
|
|--------|---------|
|
|
| `sync_known_heads` | Number of peer heads known locally. |
|
|
| `sync_lag_seconds` | Wall-clock age of the newest unapplied trusted head. |
|
|
| `sync_missing_blocks` | Blocks wanted but not available locally. |
|
|
| `sync_conflicts_total` | Unresolved conflict records by type. |
|
|
| `sync_repair_bytes_total` | Bytes repaired from object store or peers. |
|
|
| `sync_verify_failures_total` | Hash or signature verification failures. |
|
|
| `sync_fallback_local_total` | Times configured shared storage fell back to local. |
|
|
|
|
## Phased Delivery
|
|
|
|
| Phase | Scope | Acceptance |
|
|
|-------|-------|------------|
|
|
| 1. Shared object-store safe mode | Single-writer immutable objects, per-site heads, local metadata import | Two daemons can read the same corpus through R2/S3 without sharing SQLite. |
|
|
| 2. Snapshot export/import | Signed checkpoint export/import for offline transfer | Import is idempotent and detects missing blocks before catalog mutation. |
|
|
| 3. P2P block repair | Wire `RepairStrategy::FromP2P` to authenticated block fetch | Missing block repair succeeds only after hash verification and source attribution. |
|
|
| 4. Authenticated daemon sync | `hello/heads/want/have/fetch/apply/ack` between trusted peers | Network partition and reconnect converge without data loss. |
|
|
| 5. Multi-writer conflict UX | Conflict records, tombstone retention, selective sync filters | Concurrent path/tag/metadata edits are visible and resolvable. |
|
|
|
|
## Test Plan
|
|
|
|
| Category | Cases |
|
|
|----------|-------|
|
|
| Unit | Manifest hash verification, op ID idempotency, signature rejection, OR-set tag merge, tombstone retention. |
|
|
| Integration | Two local daemons sharing object store, import after missing block, remote fallback refusal, vector-cache mismatch rebuild. |
|
|
| Fault injection | Truncated object, wrong hash, stale head, concurrent heads, unavailable object store, partial multipart upload. |
|
|
| Consistency | Partition peers, perform concurrent edits, reconnect, assert deterministic conflict records. |
|
|
| Performance | Sync round bounded by head/checkpoint/object wants, not full bucket list. |
|
|
|
|
## Research Anchors
|
|
|
|
PaperBridge searches on 2026-05-24 informed the design constraints:
|
|
|
|
| Topic | Anchor |
|
|
|-------|--------|
|
|
| Content-addressed Merkle data | Roy, Mukherjee, and Chaki, "Merkle DAG-based Distributed Data Model for Content-addressed Trust-less Verifiable Data," 2022, DOI `10.1109/ubmk55850.2022.9919573`. |
|
|
| Eventual consistency and versioned reconciliation | DeCandia et al., "Dynamo," SOSP 2007, DOI `10.1145/1294261.1294281`. |
|
|
| Application-aware conflict resolution | Petersen et al., "Bayou," 1996, DOI `10.1145/504479.504497`. |
|
|
| Disconnected operation | Kistler and Satyanarayanan, "Disconnected Operation in the Coda File System," DOI `10.1007/978-0-585-29603-6_19`. |
|
|
| CRDT merge structures | Preguica, Baquero, and Shapiro, "Conflict-Free Replicated Data Types CRDTs," 2018, DOI `10.1007/978-3-319-63962-8_185-1`. |
|
|
| Untrusted global storage | Kubiatowicz et al., "OceanStore," 2000, DOI `10.1145/356989.357007`. |
|
|
| IPFS-style Merkle indexing with CRDTs | Shi et al., "IPFS Keyword Retrieval System Based on Merkle DAG Inverted Index," 2024, DOI `10.62051/ijcsit.v2n2.16`. |
|