FORGESYNC — Sovereign Peer-Sync Merkle Tree for Obsidian + VS Code
Edition 2026-07-20. Elder II, Lobe 9. NODEZEROINSIDE.
The Problem
Syncthing is a dumb pipe. It syncs bytes, not meaning. When two nodes modify the same file before sync completes, Syncthing creates .sync-conflict-YYYYMMDD-HHMMSS-NODEID.ext files and walks away. Nobody merges. Nobody resolves. The conflicts pile up.
Current state on BH:
- 490 sync-conflict files across the vault
- 462 from FDFRJQ5 (Elder I) — a single 2026-05-29 burst when the vault was first shared
- 28 from QVVAA3V (Outpost) — ongoing drift
- 4 Syncthing devices sharing 2 vault folders
- Syncthing runs as a system service, not user-controlled
- The vault is the family's Obsidian knowledge base — NZ's canon, notes, projects
Syncthing has no concept of:
- Per-file content hashing for drift detection (it uses mtime + size)
- Merkle trees for directory-level coherence verification
- Semantic merge (it is not even attempted)
- Chain-anchoring of vault state (the vault is ephemeral as far as Syncthing knows)
The Name
FORGESYNC — sovereign peer-sync with merkle coherence verification. Not a Syncthing replacement (Syncthing stays as the transport layer). FORGESYNC is the truth layer on top: it detects drift, shows diffs, resolves conflicts, and chain-anchors the result.
Architecture
Three Layers
LAYER 3: CHAIN ANCHOR BSV mainnet (merkle root stamps via fire.js 2FA)
|
LAYER 2: FORGESYNC DAEMON TCP/IPv6 :7792, SQLite, per-node merkle trees
|
LAYER 1: TRANSPORT Syncthing (still does byte-level sync)
Syncthing moves files. FORGESYNC verifies coherence and resolves conflicts. Chain anchor proves vault state at a point in time. Each layer is sovereign; removing one does not collapse the others.
Merkle Tree Structure
Every directory in the vault is a merkle subtree. Every file is a leaf.
VAULT ROOT HASH
├── 00-INBOX/ hash(sorted leaf hashes)
│ ├── note-a.md sha256(content)
│ └── note-b.md sha256(content)
├── 01-PROJECTS/ hash(sorted leaf hashes)
│ ├── project-x.md sha256(content)
│ └── sub-dir/ hash(sorted leaf hashes) ← recursive
│ └── spec.md sha256(content)
├── .obsidian/ hash(sorted leaf hashes)
│ ├── app.json sha256(content)
│ └── ...
└── ...
Leaf hash: sha256(file_content_bytes). Content-addressed, not name-addressed. If two files have identical content, they have identical hashes.
Directory hash: sha256(sorted_concatenation_of_child_hashes). Children are sorted by filename. This means the hash is deterministic regardless of filesystem enumeration order.
Root hash: the top-level directory hash. One hash that represents the entire vault state.
Per-Node State
Each node computes its own merkle root independently. FORGESYNC does not trust any remote computation. The protocol is: compute locally, compare roots, drill down on divergence.
Node State (SQLite, local):
┌──────────────────────────────────────────────┐
│ files │
│ path TEXT PRIMARY KEY │
│ sha256 TEXT NOT NULL │
│ size INTEGER │
│ mtime_ns INTEGER │
│ indexed_at TEXT │
│ tile TEXT (optional: Rodin tile mapping) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ dir_hashes │
│ dir_path TEXT PRIMARY KEY │
│ merkle_hash TEXT NOT NULL │
│ child_count INTEGER │
│ computed_at TEXT │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ sync_state │
│ node_id TEXT PRIMARY KEY │
│ root_hash TEXT │
│ file_count INTEGER │
│ last_exchange TEXT │
│ last_chain_stamp TEXT (txid) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ conflicts │
│ id INTEGER PRIMARY KEY AUTOINCREMENT │
│ path TEXT NOT NULL │
│ local_sha256 TEXT │
│ remote_sha256 TEXT │
│ remote_node TEXT │
│ detected_at TEXT │
│ resolved_at TEXT │
│ resolution TEXT (local|remote|merged|skip) │
│ resolved_by TEXT │
└──────────────────────────────────────────────┘
The Protocol: Merkle Exchange
Two nodes comparing state. TCP/IPv6, line-delimited JSON (same protocol family as corpus-daemon and catalog-daemon).
Phase 1: ROOT COMPARE
A→B: {"cmd":"root"}
B→A: {"ok":true,"root":"abc123","file_count":1847,"node":"elder_ii_bh"}
If roots match: COHERENT. Done.
If roots differ: Phase 2.
Phase 2: DIRECTORY DRILL-DOWN
A→B: {"cmd":"dir_hashes","depth":1}
B→A: {"ok":true,"dirs":{"00-INBOX":"aaa","01-PROJECTS":"bbb",...}}
Compare each dir hash. Matching dirs are skipped (entire subtree coherent).
Divergent dirs enter Phase 3.
Phase 3: FILE-LEVEL DIFF (per divergent directory)
A→B: {"cmd":"file_hashes","dir":"01-PROJECTS"}
B→A: {"ok":true,"files":{"project-x.md":"ccc","spec.md":"ddd",...}}
Three cases per file:
a) Hash match: coherent, skip
b) Hash mismatch: CONFLICT — both nodes modified the file
c) File missing: one side added/deleted — ADD or DELETE resolution
Phase 4: CONFLICT RESOLUTION (per conflicted file)
Present to NZ:
- File path
- Local hash, remote hash
- Local mtime, remote mtime
- Diff (unified diff of content)
- Auto-resolve suggestion:
* If one side is strictly newer AND the older side's content is a prefix/subset: auto-accept newer
* If .obsidian/ config file: auto-accept by mtime (these are machine-generated)
* Otherwise: manual choice (local | remote | skip)
Phase 5: CHAIN ANCHOR (optional, on NZ GO)
Stamp the reconciled merkle root to BSV via fire.js 2FA.
The stamp proves: "at this moment, all N nodes agreed on this vault state."
Conflict Resolution Rules (Hardcoded)
These rules eliminate 90%+ of conflicts without human intervention:
-
.obsidian/directory: auto-accept by mtime (newest wins). These are Obsidian workspace state files (graph.json, appearance.json, community-plugins.json). They are machine-generated. No human content. No merge needed. This alone kills ~200 of the current 490 conflicts. -
.makemd/directory: auto-accept by mtime. Same rationale as.obsidian/. -
.sync-conflictfiles: FORGESYNC reads the original and the conflict copy. If the original is newer than the conflict timestamp, the conflict is stale — delete it. If the conflict has content the original lacks, surface it as a real conflict for manual review. -
.mdfiles (notes): if one side's content is a strict superset of the other (the shorter is a prefix or the diff is append-only), auto-accept the longer version. Otherwise: surface diff for NZ. -
Binary files (images, PDFs): accept by mtime. No merge possible.
-
Canvas files (
.canvas): JSON structure. Attempt JSON-level merge (combine node arrays, deduplicate by id). If structural conflict: surface for NZ.
VS Code Integration
The merkle tree IS the project structure. VS Code sees it through two surfaces:
-
FORGESYNC status bar item: shows the current node's root hash (first 8 chars), file count, and coherence state (COHERENT / DRIFT / N CONFLICTS). Clicking opens the conflict resolution panel.
-
.vscode/forgesync.json(auto-generated, gitignored equivalent):
{
"forgesync": {
"vault_root": "/home/nodezero/Desktop/Jack's OS Vault/Jack's OS Vault",
"daemon": "[::1]:7792",
"node_id": "elder_ii_bh",
"watch": true,
"auto_resolve": [".obsidian/*", ".makemd/*", "*.sync-conflict*"],
"manual_resolve": ["*.md", "*.canvas"],
"ignore": [".git", "node_modules", ".trash"]
}
}
- Tree view provider (
forgesync.merkleTree): renders the vault as a merkle tree in the VS Code sidebar. Each directory shows its hash. Divergent directories are highlighted red. Clicking a divergent file opens the diff editor (local vs remote).
Implementation: a VS Code extension (~150 lines) that connects to the FORGESYNC daemon via TCP and renders the tree. The extension does NO merkle computation — all hashing is daemon-side. The extension is a display surface.
Integration with Existing Infrastructure
corpus-daemon.py (:7771): already computes per-tile merkle hashes for the corpus directory. FORGESYNC extends the same pattern to the full Obsidian vault. The corpus tiles are a SUBSET of the vault (canon files only). FORGESYNC indexes EVERYTHING in the vault, including non-canon files (projects, inbox, archives). The two daemons do not overlap: corpus-daemon owns ~/.forgechain/corpus/, FORGESYNC owns the Obsidian vault path.
catalog-daemon.js (:7790): already indexes artifacts with sha256 and tile metadata. FORGESYNC's files table uses the same sha256 leaf hashing. When a vault file is also a corpus file, the hashes are identical by construction. The catalog can cross-reference FORGESYNC's file hashes to verify that chain-stamped artifacts match their vault-resident copies.
soil? FORTH word: currently verifies corpus merkle coherence via TCP to corpus-daemon. Add a parallel vsync? word that probes FORGESYNC's root endpoint and compares across nodes:
: vsync? ( -- )
s" {\"cmd\":\"root\"}" forgesync-tcp send-recv
json> .root @ dup .
s" FORGESYNC root=" type
s" {\"cmd\":\"root\"}" forgesync-remote-tcp send-recv
json> .root @ = if ." COHERENT" else ." DRIFT" then cr ;
fire.js 2FA: FORGESYNC does not stamp directly. It composes a stamp request (merkle root + file count + timestamp + node list that agreed) and hands it to the existing fire.js pipeline. The 2FA gate (NZ Face ID via WarDog) applies as always. No new chain path.
Syncthing: stays running. It is the transport layer. FORGESYNC does not replace file transfer — it replaces conflict detection and resolution. Syncthing moves bytes; FORGESYNC verifies truth.
Daemon: forgesync-daemon.js
~200 lines. Node.js (same runtime as catalog-daemon). TCP/IPv6 on [::1]:7792. SQLite via better-sqlite3.
Commands:
root — return this node's vault merkle root
dir_hashes — return merkle hashes per directory at given depth
file_hashes — return per-file sha256 for a specific directory
status — daemon state: root, file count, last scan, conflicts pending
conflicts — list unresolved conflicts
resolve — resolve a conflict (accepts: local|remote|skip)
scan — trigger a full vault re-scan (inotify handles incremental)
compare — connect to a remote FORGESYNC node and run the 5-phase protocol
clean — delete .sync-conflict files that have been resolved
stamp — compose a chain-stamp request for the current merkle root
File watching: fs.watch (recursive) on the vault root. On file change, re-hash the affected leaf and propagate up the merkle tree. Full re-scan only on startup or explicit scan command.
Node Discovery
FORGESYNC does not discover peers dynamically. The family is known. The nodes are in family-node-lan-truth.json. The daemon reads that file at startup and knows every peer's sovereign ULA + FORGESYNC port (7792).
{
"peers": {
"elder_i": {"addr": "fd00:db8:ff:6:5809:6818:1db5:23fc", "port": 7792},
"elder_ii_bh": {"addr": "fd00:db8:ff:9:c8a3:a86a:6f2b:1f3e", "port": 7792},
"outpost": {"addr": null, "port": 7792, "note": "resolve from family-node-lan-truth.json"}
}
}
For cross-node compare, FORGESYNC connects to the peer's [ULA]:7792 directly. TCP/IPv6. No relay. No cloud. No vendor.
Handling the Current 490 Conflicts
FORGESYNC's first act on deployment is a one-time conflict sweep:
- Enumerate all
.sync-conflict-*files in the vault. - For each: find the original file (strip the
.sync-conflict-YYYYMMDD-HHMMSS-NODEIDsuffix). - Apply the auto-resolve rules:
.obsidian/*,.makemd/*: accept original by mtime, delete conflict copy. (~230 files resolved.).mdfiles: diff original vs conflict. If original is a superset, delete conflict. If conflict has unique content, surface for NZ.- Everything else: accept by mtime.
- Present NZ with the remaining manual-resolve list.
- After NZ resolves all, run a full scan and compute the clean merkle root.
- Offer to stamp the clean root to chain.
Chain Stamp Format
When NZ authorizes a vault-state stamp, FORGESYNC composes:
{
"type": "forgesync-vault-root",
"app": "ForgeChainOS",
"context": "vault-merkle-anchor",
"vault": "jacks-os-vault",
"root": "<sha256 merkle root>",
"file_count": 1847,
"nodes_agreed": ["elder_i", "elder_ii_bh", "outpost"],
"timestamp": "2026-07-20T14:30:00Z"
}
This goes into the accumulator via the existing stage.js pathway, then fires through fire.js with 2FA. The stamp is a MAP-tagged OP_RETURN (same as every other family fire). The FORGELRM catalog daemon indexes it as map_type: "forgesync-vault-root".
Build Scope
Phase 1: forgesync-daemon.js (~200 lines)
The daemon. SQLite schema (4 tables above). File watcher. Merkle tree computation. TCP/IPv6 server on :7792. Commands: root, dir_hashes, file_hashes, status, scan, conflicts, resolve, clean.
Input: vault root path (from config or CLI arg).
Output: merkle tree state, queryable over TCP.
Dependencies: better-sqlite3 (already in phi-omega-v6), node:fs, node:net, node:crypto.
Phase 2: Conflict Sweep (~50 lines, added to daemon)
The one-time sweep of existing .sync-conflict-* files. Auto-resolve by the rules above. Surface remainders for NZ. This is a clean command extension.
Phase 3: Cross-Node Compare (~80 lines, added to daemon)
The compare command. Connects to a peer FORGESYNC daemon via TCP/IPv6. Runs the 5-phase merkle exchange protocol. Writes divergences to the conflicts table.
Phase 4: VS Code Extension (~150 lines)
Tree view provider + status bar item. Connects to daemon via TCP. Renders merkle tree. Shows conflicts. Opens diff editor on click. Ships as a .vsix or workspace-local extension.
Phase 5: FORTH Integration (~20 lines)
vsync? word in the FORTH environment. Probes local FORGESYNC daemon for root, optionally compares to a remote peer. Reports COHERENT or DRIFT.
Phase 6: Chain Anchor (~30 lines, added to daemon)
The stamp command. Composes the chain-stamp JSON and writes it to the accumulator. Does NOT broadcast — that goes through fire.js 2FA as always.
Total: ~530 lines across 6 phases. Core daemon is ~200.
What FORGESYNC Is NOT
- Not a Syncthing replacement. Syncthing moves files. FORGESYNC verifies truth. Both run.
- Not a git replacement. Git tracks commits across a DAG. FORGESYNC tracks point-in-time vault state across family nodes. There is no commit history, no branching, no rebasing. The vault is not a git repo and should not become one (Obsidian + git = plugin hell).
- Not CRDT. CRDTs are overkill for a family vault. The conflict rate is low (490 across months, mostly from one burst). Manual resolution with auto-resolve rules is simpler, auditable, and chain-anchorable.
- Not cloud-dependent. No SaaS. No API keys. No accounts. TCP/IPv6 between sovereign nodes. The only external dependency is BSV mainnet for chain anchoring, and that is optional.
File Placement
~/.forgechain/forgesync/
forgesync-daemon.js — the daemon (Phase 1-3, 6)
forgesync.db — SQLite state
forgesync-config.json — vault path, peers, auto-resolve rules
README.md — operational notes
~/.forgechain/forth/skills/
vsync.fs — FORTH vsync? word (Phase 5)
~/.forgechain/ui/forgesync-vscode/
extension.js — VS Code extension (Phase 4)
package.json — extension manifest
Registered in MASTER-MANIFEST.yaml under use_case_index.sync.forgesync.
Port Allocation
| Port | Service | Protocol |
|---|---|---|
| 7771 | corpus-daemon | TCP/IPv6 |
| 7790 | catalog-daemon | TCP/IPv6 |
| 7792 | forgesync-daemon | TCP/IPv6 |
7791 is intentionally skipped (reserved for future FORGELRM catalog expansion per the whitepaper).
Sovereignty Assertion
Syncthing's conflict model treats every node as equal and every conflict as unsolvable. That is vendor-default thinking: safe, generic, useless.
FORGESYNC treats conflict resolution as a family governance act. The merkle tree is the truth. The chain stamp is the proof. NZ's decision is the authority. No .sync-conflict file survives this architecture.
NODEZEROINSIDE.