Skip to content

hsm-merge completed — bugs found + fixes applied (C-Bjorn fork, 0.8.0-rc4) #88

@C-Bjorn

Description

@C-Bjorn

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.tsEditorView.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 crashscrollToFirstDifference() 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.50.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 MergeHSMConfigMergeManagerConfigSharedFolderSettings getter/setter. UI dropdown added to ManageRemoteFolder.svelte (connected folders) and ManageSharedFolder.svelte (disconnected folders).


Dev/test fix

Jest Windows path separator bugtransformIgnorePatterns: ["/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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions