daemon: relocate any non-.md file that lands in /vault to matching /data path

/vault is notes-only by design; binaries saved there by hand, by an
agent, or by a sync client now get moved to /data automatically
instead of sitting in the wrong tree. Wired into live inotify (file
create + file move) and the periodic reconcile sweep so pre-existing
drift self-heals too. Leaves the vault copy in place with a warning
ntfy if a same-named file already exists at the /data destination.
This commit is contained in:
Hermes
2026-09-25 20:14:32 -04:00
parent cf6196d525
commit 1938b889e2
+61 -3
View File
@@ -8,6 +8,9 @@ structurally mirrored, in BOTH directions, for changes made by ANY tool
Rules (confirmed with Ryan 2026-09-15): Rules (confirmed with Ryan 2026-09-15):
FORWARD (/vault -> /data): FORWARD (/vault -> /data):
- any non-`.md` file created or moved INTO /vault (a binary dropped or
saved there by mistake -- /vault is notes-only by design) -> relocate
it to the matching path under /data instead of leaving it in /vault.
- rename/move of an ID folder in /vault -> mirror the rename in /data. - rename/move of an ID folder in /vault -> mirror the rename in /data.
- delete of an ID folder in /vault -> do NOT delete /data's copy. - delete of an ID folder in /vault -> do NOT delete /data's copy.
Instead: move the /data folder into the NEAREST existing `.09` archive Instead: move the /data folder into the NEAREST existing `.09` archive
@@ -309,6 +312,49 @@ def handle_vault_moved(src_path, dest_path):
) )
def handle_vault_binary_created(path):
"""
/vault is notes-only by design (see vault-librarian skill). Any
non-`.md` file that lands there -- Ryan saving a photo/PDF by hand,
an agent writing to the wrong tree, a sync client dropping a stray
file -- gets relocated to the matching path under /data, never left
in /vault. Idempotent: no-op if the file is already gone (raced with
another mover) or the /data destination already exists (name clash --
log and leave the vault copy in place rather than silently overwriting
or deleting real content).
"""
if not os.path.isfile(path):
return
if path.endswith(".md"):
return
if _in_skip(path, VAULT):
return
if not _debounced("vault_binary_created", path):
return
rel = os.path.relpath(path, VAULT)
if rel.split(os.sep)[0] == vl.SYSTEM_META:
return # "0 System area" has no /data counterpart at all
data_path = os.path.join(DATA, rel)
if os.path.exists(data_path):
log(f"SKIP relocate (already exists in /data, leaving vault copy): {rel}")
ntfy_post(
"Vault sync: binary needs manual move",
f"Non-.md file in /vault couldn't relocate -- /data already "
f"has {rel}. Left in place, resolve by hand.",
tags="warning",
priority="high",
)
return
os.makedirs(os.path.dirname(data_path), exist_ok=True)
shutil.move(path, data_path)
log(f"RELOCATED non-md file /vault -> /data: {rel}")
ntfy_post(
"Vault sync: moved binary out of /vault",
f"{rel} isn't a note -- moved to /data.",
tags="package",
)
def handle_journal_file_created(path): def handle_journal_file_created(path):
""" """
A new dated journal entry (YYYY-MM-DD.md) appeared under R.16.11 -- A new dated journal entry (YYYY-MM-DD.md) appeared under R.16.11 --
@@ -689,6 +735,7 @@ class VaultHandler(FileSystemEventHandler):
def on_moved(self, event): def on_moved(self, event):
if not event.is_directory: if not event.is_directory:
_event_queue.put(("vault_file_moved", event.src_path, event.dest_path))
return return
_event_queue.put(("vault_moved", event.src_path, event.dest_path)) _event_queue.put(("vault_moved", event.src_path, event.dest_path))
@@ -728,7 +775,11 @@ def _worker_loop():
elif kind == "vault_file_created": elif kind == "vault_file_created":
_, path = item _, path = item
time.sleep(0.5) # let the writer (Obsidian/editor) finish writing content time.sleep(0.5) # let the writer (Obsidian/editor) finish writing content
handle_vault_binary_created(path)
handle_journal_file_created(path) handle_journal_file_created(path)
elif kind == "vault_file_moved":
_, src_path, dest_path = item
handle_vault_binary_created(dest_path)
elif kind == "vault_moved": elif kind == "vault_moved":
_, src_path, dest_path = item _, src_path, dest_path = item
handle_vault_moved(src_path, dest_path) handle_vault_moved(src_path, dest_path)
@@ -797,7 +848,8 @@ def reconcile():
handle_data_created(dirpath) handle_data_created(dirpath)
data_count += 1 data_count += 1
vault_count = 0 vault_count = 0
for dirpath, dirnames, _ in os.walk(VAULT): vault_file_count = 0
for dirpath, dirnames, filenames in os.walk(VAULT):
dirnames.sort() dirnames.sort()
if dirpath == VAULT: if dirpath == VAULT:
continue continue
@@ -807,8 +859,14 @@ def reconcile():
with _lock: with _lock:
handle_vault_created(dirpath) handle_vault_created(dirpath)
vault_count += 1 vault_count += 1
log(f"reconcile: swept {data_count} /data dirs, {vault_count} /vault dirs " for fname in filenames:
f"in {time.time() - t0:.1f}s") if fname.endswith(".md"):
continue
with _lock:
handle_vault_binary_created(os.path.join(dirpath, fname))
vault_file_count += 1
log(f"reconcile: swept {data_count} /data dirs, {vault_count} /vault dirs, "
f"{vault_file_count} /vault non-md files in {time.time() - t0:.1f}s")
def _reconcile_loop(): def _reconcile_loop():