Skip to content

fix(graph): tombstones are terminal — re-derivation cannot resurrect facts - #69

Merged
alpertarhan merged 1 commit into
mainfrom
fix/issue-66-tombstone-terminal
Sep 17, 2026
Merged

alpertarhan merged 1 commit into
mainfrom
fix/issue-66-tombstone-terminal

Conversation

@alpertarhan

Copy link
Copy Markdown
Owner

Fixes #66.

Root cause

markFactStatus writes the resolved/superseded tombstone onto the same deterministic row id (stableId(projectId, sessionId, kind, factKey, branchHeadId)) that the ingest path computes for the same fact. But the write path's dedup check, sameActiveFact, requires row.status === "active" — a tombstoned row never matches, so the next compaction that re-derived the same content from the transcript hit the ON CONFLICT(id) DO UPDATE SET status = excluded.status upsert and flipped the fact back to active.

Net effect: /smart-compact loops resolve (and extraction-driven resolves) silently undid themselves on the next pass.

Fix

Both ingest write paths — addFact and the open-loops block — now fetch the latest lineage fact once and, in addition to the existing active-dedup check, skip the write when the latest lineage fact is non-active and the incoming node is active:

  • tombstones (resolved/superseded) are terminal for their lineage
  • writes that themselves carry a non-active status (extraction resolving a loop, factOverrides) are unaffected
  • sibling branches keep their isolation — the check goes through latestLineageFact, which already ranks by branch lineage

Semantics note

Content-addressed factKey cannot distinguish a genuine recurrence from a transcript echo. This PR deliberately makes resolve terminal per lineage; re-opening requires an explicit override (a future unresolve), not passive re-ingest. This is also the foundation #63's forget needs — deletion must not be resurrectable through the same path.

Verification

  • 987 tests (2 new: resolved error re-derived as unresolved stays hidden; resolved loop re-reported as open stays hidden), bench, build, release audit — all green

…facts (#66)

The ingest write path (addFact and the open-loops block) only deduped
against ACTIVE lineage facts, so a resolved/superseded row never matched
its own tombstone and the deterministic upsert id flipped status back to
active whenever a later compaction pass re-derived the same content from
the transcript.

Both paths now skip active writes when the latest lineage fact for the
same kind+factKey is non-active. Writes that themselves carry a non-
active status (extraction resolving a loop, fact overrides) are
unaffected, and sibling-branch isolation is preserved.
@alpertarhan
alpertarhan merged commit eee04de into main Sep 17, 2026
2 checks passed
@alpertarhan
alpertarhan deleted the fix/issue-66-tombstone-terminal branch September 17, 2026 18:42
alpertarhan added a commit that referenced this pull request Sep 17, 2026
…) (#70)

- forgetProjectGraph(projectId): drains queued index writes first (so a
  scheduled compaction cannot resurrect the memory), then deletes FTS
  copies, edges, and nodes in one transaction
- /smart-compact forget: cwd-resolves the project, shows node/session
  counts, requires a TUI confirmation dialog; compaction state (restore
  data) and backups are deliberately kept
- tombstone-terminal semantics from #69 guarantee deleted facts cannot
  be re-derived back into existence by later compactions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolved loop facts get re-activated by later compaction passes (tombstones ignored on write path)

1 participant