Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

bug(obsidian): obsidian-export leaves stale notes for hard-deleted and renamed observations

Open
#1,900 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
62/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
go
Domain
cli, data

Research direction

Start in internal/obsidian/exporter.go: the cleanup loop only visits rows from store.Export() with DeletedAt set, and the export loop overwrites state.Files[obs.ID] without comparing the old path. Reproduce with the numbered steps (hard delete, then a PATCH rename, then export) and check .engram-sync-state.json. Done when hard-deleted and renamed notes are removed or moved, orphaned hubs are pruned, and the export output reports the deletions.

Written by the indexing model from the issue text.

Description

Existing Issue Search
  • I searched existing issues and will add evidence to a canonical tracker when applicable.
📝 Observed Consequence

engram obsidian-export leaves Markdown notes in the vault that no longer correspond to any live observation, and keeps reporting Deleted: 0. Two triggers:

  1. Hard-deleted observations (engram delete <id> --hard, engram delete project <name> --hard, or any hard deletion that happens between two exports): the note, and its session hub, stay in the vault forever. Their entry also stays in .engram-sync-state.json.
  2. Renamed observations (title updated, so the slug changes): the next export writes a new file and overwrites the state entry with the new path. The old file stays on disk and is no longer tracked by the state, so not even a state-based cleanup can find it.

Soft deletion works correctly: engram delete <id> followed by an export removes the note (Deleted: 1).

On a real vault, after a bulk hard deletion and normal use, this accumulated several hundred stale notes plus orphan session and topic hubs, all shown in Obsidian search and Graph View as if they were live memories.

🔄 Steps to Reproduce
  1. Isolated store and vault: T=$(mktemp -d); e() { HOME="$T/home" ENGRAM_DATA_DIR="$T/data" engram "$@"; }
  2. e save "keep me" "first body" --project demo and e save "hard gone" "second body" --project demo
  3. e obsidian-export --vault "$T/vault" --all --graph-config skip → Created: 2
  4. e delete 2 --hard
  5. e obsidian-export --vault "$T/vault" --all --graph-config skip → Deleted: 0; engram/demo/manual/hard-gone-2.md still exists and .engram-sync-state.json still maps "2" to it.
  6. Rename observation 1 through the HTTP API: e serve 17499 &, then curl -X PATCH 'localhost:17499/observations/1?expected_project=demo' -H 'content-type: application/json' -d '{"title":"keep me renamed"}', then stop the server.
  7. e obsidian-export --vault "$T/vault" --all --graph-config skip → Created: 1, Deleted: 0; both keep-me-1.md and keep-me-renamed-1.md exist, and the state only knows keep-me-renamed-1.md.
✅ Expected Behavior

After an export, the vault contains exactly one note per live observation:

  • Notes tracked in the state whose observation no longer exists in the store at all (hard-deleted) are removed, like soft-deleted ones.
  • When an observation's target path changes, the previously tracked file is removed (or moved) before the state entry is overwritten.
  • Session/topic hubs that no longer reference any live observation are removed.
❌ Actual Behavior
# after hard delete of #2
Obsidian export complete
  Created: 0
  Updated: 0
  Deleted: 0
  Skipped: 1
  Hubs:    1
engram/demo/manual/hard-gone-2.md   <- still present
engram/demo/manual/keep-me-1.md

# after renaming #1
  Created: 1
  Updated: 0
  Deleted: 0
engram/demo/manual/keep-me-renamed-1.md
engram/demo/manual/keep-me-1.md     <- stale, untracked
engram/demo/manual/hard-gone-2.md   <- still present
Operating System

macOS

Release Version (Report Context)

3.4.0

Agent / Client

Other

📋 Relevant Logs

No response

💡 Additional Context

Client: Pi (via gentle-engram); the exporter is client-independent.

Current main (8589a5f), internal/obsidian/exporter.go:

  • The cleanup loop only visits rows returned by store.Export() that have DeletedAt != nil. Hard-deleted rows are not returned, so their state.Files entries are never reconciled.
  • The export loop computes relPath from the current slug and assigns state.Files[obs.ID] = relPath without comparing it with the previously tracked path.

A possible approach: after the export loop, remove every tracked state.Files entry whose ID is not a live observation, and when the tracked path for a live ID differs from the new relPath, delete the old file (with the same containment checks used today).

Workaround: compare the state's file IDs with live observation IDs, then delete the orphaned files and prune the state by hand, after taking a backup.

Dominant language
Go
Stars
7.1k
Forks
729
Avg merge
11h 17m
Merged PRs (30d)
334

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Gentleman-Programming/engram

All issues in Gentleman-Programming/engram

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.