Hey — I integrated the merge-hsm branch from PR #69 into my fork and ran it live for a few days against Oracle88Ministries (a shared folder used by MegaMem, Claude Code, and 2 collaborators). Found a few bugs and added a feature. Not submitting a PR since I figure the branch is progressing anyway, but wanted to flag what I found in case any of it is useful to cherry-pick.
What I integrated
Starting point: integration/merge-hsm as of 8a811be (same CI commit as f940558 in my fork history).
My fork: C-Bjorn/Relay @ integration/merge-hsm
Release: C-Bjorn/Relay releases/tag/0.8.0-rc4
Bugs Found & Fixed
🐛 1 — External writes not syncing when file is closed (sub-case B)
Root cause: vault.on("modify") handler only sent DISK_CHANGED when file.hsm is non-null. For files where the Document was created before MergeManager was initialized (race at startup), file.hsm remains null and DISK_CHANGED is silently dropped.
Fix:
- Added
MergeManager.getHSMForGuid(guid: string): MergeHSM | null public method
- After the existing
file.hsm && block in main.ts vault.on("modify"), added a !file.hsm fallback that reaches into the MergeManager to find the registered HSM and deliver DISK_CHANGED directly
// src/main.ts — after existing file.hsm block
if (file && isDocument(file) && !file.hsm && !file.isSaving && tfile instanceof TFile) {
const hsm = folder.mergeManager?.getHSMForGuid(file.guid);
if (hsm) {
const contents = await this.app.vault.read(tfile);
const encoder = new TextEncoder();
const hash = await generateHash(encoder.encode(contents).buffer);
hsm.send({ type: 'DISK_CHANGED', contents, mtime: tfile.stat.mtime, hash });
}
}
Also removed the orphaned import { diffMatchPatch } from main.ts (was referenced in an earlier approach, no longer called).
🐛 2 — Conflict UI labels backwards ("Editor"/"Disk" should be "Remote"/"Local")
Root cause: Three sites in MergeHSM.ts hardcode oursLabel: "Editor" / theirsLabel: "Disk" for the file diff header. In Relay's architecture, "Editor" = Y.js CRDT = server/remote state, "Disk" = local files = what external tools write. The inline per-hunk buttons already say "Accept Local" / "Accept Remote" correctly — the inconsistency was only in the header buttons.
Fix: Changed all 3 sites in MergeHSM.ts to "Remote" / "Local". Also updated fallback labels in differencesView.ts (?? "Editor" → ?? "Remote", ?? "Disk" → ?? "Local").
🐛 3 — Hardcoded CSS colors break dark themes; @media prefers-color-scheme mismatch
Two problems:
a) ConflictDecorationPlugin.ts — EditorView.baseTheme() uses Bootstrap rgba() hardcodes (rgba(40, 167, 69, 0.12), rgba(0, 123, 255, 0.12), rgba(108, 117, 125, 0.2)) that ignore the active Obsidian theme.
Fix: Replace with color-mix(in srgb, var(--color-green) 20%, transparent), color-mix(in srgb, var(--interactive-accent) 20%, transparent), color-mix(in srgb, var(--text-muted) 20%, transparent). Also added color: var(--text-normal) to content divs and updated separator text from Remote/Disk to Remote / Local.
b) styles.css — The file diff view uses @media (prefers-color-scheme: dark) which breaks when Obsidian is in dark mode but the OS is in light mode (common with the Minimal theme).
Fix: Removed both @media blocks. Replaced with universal color-mix() rules using Obsidian CSS variables:
.file-diff__top-line__bg {
background-color: color-mix(in srgb, var(--color-green) 15%, transparent);
}
.file-diff__bottom-line__bg {
background-color: color-mix(in srgb, var(--interactive-accent) 12%, transparent);
}
c) differencesView.ts crash — scrollToFirstDifference() crashes with TypeError: Cannot read properties of undefined (reading 'getBoundingClientRect') when there are no .difference elements yet rendered. Added null guards:
const container = this.contentEl.getElementsByClassName("file-diff__container")[0];
const element = this.contentEl.getElementsByClassName("difference")[0];
if (!container || !element) return;
🐛 4 — Re-appearing conflicts after upgrade (HSMStore not migrated)
After upgrading from 0.7.5 → 0.8.0-rc3, the Y.js IDB store was renamed (relay-folder-{guid} → {appId}-relay-folder-{guid}) but the HSMStore (separate IDB database holding LCA, fork, deferredConflict) was not migrated. Fresh HSMs start with no LCA → treats any disk/CRDT difference as an unresolvable diverge → false conflicts on every file open.
Immediate fix added (button): "Reset Sync State" button in the Shared Folder settings panel (under Maintenance heading). Calls HSMStore.clearAllData() → wipes LCA/fork/deferredConflict → HSMs reinitialize cleanly on next open.
Longer-term: The migrateFrom path in y-indexeddb should also migrate HSMStore records to the new namespace.
Feature Added
✨ Per-folder autoResolveConflicts setting
For folders primarily written by AI tools (MegaMem, Claude Code), added a per-folder setting to auto-resolve conflicts without user interaction:
'none' (default) — show conflict UI
'remote' — silently accept CRDT/server version when conflict detected; disk updated
'local' — silently accept disk/external write version; pushed to server
Technical note: This fires in invokeIdleThreeWayAutoMerge() only when diff3Merge returns a genuine conflict (mergeResult.success === false). The diff3 algorithm operates at line level (s.split(/(\n)/)), so two concurrent appends at EOF auto-merge regardless of setting; two sides changing the same line from the same LCA base triggers the auto-resolve path.
Implementation: getAutoResolveConflicts?: () => 'none' | 'remote' | 'local' callback threaded through MergeHSMConfig → MergeManagerConfig → SharedFolderSettings getter/setter. UI dropdown added to ManageRemoteFolder.svelte (connected folders) and ManageSharedFolder.svelte (disconnected folders).
Dev/test fix
Jest Windows path separator bug — transformIgnorePatterns: ["/node_modules/(?!(yjs|lib0)/)"] and the transform key "node_modules/(yjs|lib0)/.+\\.js$" use forward-slash paths that don't match Windows backslash paths (\node_modules\). Changed to [\\/]node_modules[\\/]. This unlocked 689 previously blocked tests — test suite went from 89/89 on 6 suites to 778/778 on 32 suites on Windows.
All changes are on integration/merge-hsm in my fork. Happy to discuss any of these if useful. The autoResolveConflicts feature in particular has been a game-changer for our AI-writer workflow.
Hey — I integrated the
merge-hsmbranch from PR #69 into my fork and ran it live for a few days against Oracle88Ministries (a shared folder used by MegaMem, Claude Code, and 2 collaborators). Found a few bugs and added a feature. Not submitting a PR since I figure the branch is progressing anyway, but wanted to flag what I found in case any of it is useful to cherry-pick.What I integrated
Starting point:
integration/merge-hsmas of8a811be(same CI commit asf940558in my fork history).My fork: C-Bjorn/Relay @
integration/merge-hsmRelease: C-Bjorn/Relay releases/tag/0.8.0-rc4
Bugs Found & Fixed
🐛 1 — External writes not syncing when file is closed (sub-case B)
Root cause:
vault.on("modify")handler only sentDISK_CHANGEDwhenfile.hsmis non-null. For files where the Document was created beforeMergeManagerwas initialized (race at startup),file.hsmremains null andDISK_CHANGEDis silently dropped.Fix:
MergeManager.getHSMForGuid(guid: string): MergeHSM | nullpublic methodfile.hsm &&block inmain.ts vault.on("modify"), added a!file.hsmfallback that reaches into the MergeManager to find the registered HSM and deliverDISK_CHANGEDdirectlyAlso removed the orphaned
import { diffMatchPatch }frommain.ts(was referenced in an earlier approach, no longer called).🐛 2 — Conflict UI labels backwards ("Editor"/"Disk" should be "Remote"/"Local")
Root cause: Three sites in
MergeHSM.tshardcodeoursLabel: "Editor"/theirsLabel: "Disk"for the file diff header. In Relay's architecture, "Editor" = Y.js CRDT = server/remote state, "Disk" = local files = what external tools write. The inline per-hunk buttons already say "Accept Local" / "Accept Remote" correctly — the inconsistency was only in the header buttons.Fix: Changed all 3 sites in
MergeHSM.tsto"Remote"/"Local". Also updated fallback labels indifferencesView.ts(?? "Editor"→?? "Remote",?? "Disk"→?? "Local").🐛 3 — Hardcoded CSS colors break dark themes;
@media prefers-color-schememismatchTwo problems:
a)
ConflictDecorationPlugin.ts—EditorView.baseTheme()uses Bootstraprgba()hardcodes (rgba(40, 167, 69, 0.12),rgba(0, 123, 255, 0.12),rgba(108, 117, 125, 0.2)) that ignore the active Obsidian theme.Fix: Replace with
color-mix(in srgb, var(--color-green) 20%, transparent),color-mix(in srgb, var(--interactive-accent) 20%, transparent),color-mix(in srgb, var(--text-muted) 20%, transparent). Also addedcolor: var(--text-normal)to content divs and updated separator text fromRemote/DisktoRemote / Local.b)
styles.css— The file diff view uses@media (prefers-color-scheme: dark)which breaks when Obsidian is in dark mode but the OS is in light mode (common with the Minimal theme).Fix: Removed both
@mediablocks. Replaced with universalcolor-mix()rules using Obsidian CSS variables:c)
differencesView.tscrash —scrollToFirstDifference()crashes withTypeError: Cannot read properties of undefined (reading 'getBoundingClientRect')when there are no.differenceelements yet rendered. Added null guards:🐛 4 — Re-appearing conflicts after upgrade (HSMStore not migrated)
After upgrading from
0.7.5→0.8.0-rc3, the Y.js IDB store was renamed (relay-folder-{guid}→{appId}-relay-folder-{guid}) but the HSMStore (separate IDB database holding LCA, fork, deferredConflict) was not migrated. Fresh HSMs start with no LCA → treats any disk/CRDT difference as an unresolvable diverge → false conflicts on every file open.Immediate fix added (button): "Reset Sync State" button in the Shared Folder settings panel (under Maintenance heading). Calls
HSMStore.clearAllData()→ wipes LCA/fork/deferredConflict → HSMs reinitialize cleanly on next open.Longer-term: The
migrateFrompath iny-indexeddbshould also migrate HSMStore records to the new namespace.Feature Added
✨ Per-folder
autoResolveConflictssettingFor folders primarily written by AI tools (MegaMem, Claude Code), added a per-folder setting to auto-resolve conflicts without user interaction:
'none'(default) — show conflict UI'remote'— silently accept CRDT/server version when conflict detected; disk updated'local'— silently accept disk/external write version; pushed to serverTechnical note: This fires in
invokeIdleThreeWayAutoMerge()only whendiff3Mergereturns a genuine conflict (mergeResult.success === false). Thediff3algorithm operates at line level (s.split(/(\n)/)), so two concurrent appends at EOF auto-merge regardless of setting; two sides changing the same line from the same LCA base triggers the auto-resolve path.Implementation:
getAutoResolveConflicts?: () => 'none' | 'remote' | 'local'callback threaded throughMergeHSMConfig→MergeManagerConfig→SharedFolderSettingsgetter/setter. UI dropdown added toManageRemoteFolder.svelte(connected folders) andManageSharedFolder.svelte(disconnected folders).Dev/test fix
Jest Windows path separator bug —
transformIgnorePatterns: ["/node_modules/(?!(yjs|lib0)/)"]and thetransformkey"node_modules/(yjs|lib0)/.+\\.js$"use forward-slash paths that don't match Windows backslash paths (\node_modules\). Changed to[\\/]node_modules[\\/]. This unlocked 689 previously blocked tests — test suite went from 89/89 on 6 suites to 778/778 on 32 suites on Windows.All changes are on
integration/merge-hsmin my fork. Happy to discuss any of these if useful. TheautoResolveConflictsfeature in particular has been a game-changer for our AI-writer workflow.