Skip to content

fix(store): relations missing a sync_mutations row never reach cloud (no relation backfill) #496

@forNerzul

Description

@forNerzul

📋 Pre-flight Checks

  • I have searched existing issues and this is not a duplicate
  • I understand this issue needs status:approved before a PR can be opened

📝 Bug Description

A relation can exist in the local memory_relations table with no corresponding sync_mutations row, and there is no mechanism to ever create one. Such relations are silently never replicated to Engram Cloud — even though the local chunk sync path (post-#489/#494) exports every non-orphaned relation directly from the table.

This is distinct from the already-resolved relation cloud issues, all of which operate on relation mutations that already exist in the journal:

None of these create a sync_mutations row for a relation that never had one. The cloud's own row-creation/repair mechanism, backfillProjectSyncMutationsTx, backfills sessions/observations/prompts but never relations.

Root cause

Three confluent code paths, all verified against engram 1.16.3:

  1. No relation backfill. backfillProjectSyncMutationsTx (internal/store/store.go:4996) calls only backfillSessionSyncMutationsTx, backfillObservationSyncMutationsTx, and backfillPromptSyncMutationsTx. projectNeedsBackfill (store.go:5009) only counts missing sessions/observations/prompts. There is no backfillRelationSyncMutationsTx anywhere in the repo. These run at startup (NewrepairEnrolledProjectSyncMutations, store.go:643) and on enrollment/rename/merge — the exact "row exists in source table but not in journal" repair that relations are excluded from.

  2. JudgeBySemantic never enqueues. JudgeBySemantic (internal/store/relations.go:732) inserts/updates memory_relations but never calls enqueueSyncMutationTx. The only relation enqueue in the codebase is relations.go:687, inside JudgeRelation. JudgeBySemantic is live: it backs mem_compare (internal/server/server.go:1315, internal/mcp/mcp.go:2134) and ScanProject (relations.go:1400). Every semantic/system verdict therefore lands in the table with no journal row.

  3. JudgeRelation skips the enqueue when the project is not enrolled. relations.go:653: if enrolled == 0 { return nil // not enrolled — no mutation enqueued }. By contrast, enqueueSyncMutationTx (store.go:5364) does not gate on enrollment — observations/sessions/prompts are enqueued unconditionally and the backfill catches legacy ones. Relations have neither: judged-before-enrollment relations get no row, and enrolling later does not backfill them.

The net asymmetry: the local chunk export reads relations straight from memory_relations (query at store.go:~3380, used by engram sync), so local sync carries them regardless of how they were judged. The cloud path is journal-based and only ever carries what was enqueued — so the same table yields different replication outcomes for local vs cloud, and for JudgeRelation vs JudgeBySemantic.

🔄 Steps to Reproduce

Path A — JudgeBySemantic (no upgrade required):

  1. Enroll a project in cloud sync.
  2. Trigger a semantic verdict that is not not_conflict — e.g. an agent calls mem_compare on two observations, or ScanProject runs.
  3. Inspect the journal:
    -- a row exists in memory_relations:
    SELECT sync_id, relation FROM memory_relations ORDER BY created_at DESC LIMIT 1;
    -- but no matching mutation was enqueued:
    SELECT COUNT(*) FROM sync_mutations WHERE entity='relation' AND entity_key='<that sync_id>';  -- 0
  4. The relation is never pushed to cloud; local sync (engram sync) would export it.

Path B — pre-enrollment JudgeRelation:

  1. With a project not enrolled, resolve a conflict candidate via mem_judge (→ JudgeRelation). relations.go:653 returns without enqueuing.
  2. Enroll the project. repairEnrolledProjectSyncMutations backfills sessions/observations/prompts but not the relation.
  3. The relation is permanently absent from the cloud journal.

✅ Expected Behavior

  • A relation present in memory_relations (non-orphaned) is eventually represented by a sync_mutations row and replicated to cloud, regardless of the judge path or enrollment ordering — matching how local chunk sync already treats relations and how the cloud already treats sessions/observations/prompts.
  • Concretely: backfillProjectSyncMutationsTx / projectNeedsBackfill cover relations, and/or JudgeBySemantic enqueues, and/or JudgeRelation enqueues even when not-yet-enrolled (so backfill can pick it up later).

❌ Actual Behavior

Relations created via JudgeBySemantic, or via JudgeRelation before enrollment, have no sync_mutations row and no repair path. They never reach Engram Cloud and fail silently — there is no warning, and the local sync path replicates them, masking the loss until a cloud-only consumer is compared.

Operating System

macOS

Engram Version

1.16.3

Agent / Client

Claude Code

📋 Relevant Logs

# Verified by source inspection (engram 1.16.3):
internal/store/store.go:4996  backfillProjectSyncMutationsTx -> session/observation/prompt only
internal/store/store.go:5009  projectNeedsBackfill           -> counts session/observation/prompt only
internal/store/store.go:5364  enqueueSyncMutationTx          -> no enrollment gate (obs/sessions/prompts always enqueue)
internal/store/relations.go:653  JudgeRelation               -> "return nil // not enrolled — no mutation enqueued"
internal/store/relations.go:687  JudgeRelation               -> the ONLY relation enqueue in the codebase
internal/store/relations.go:732  JudgeBySemantic             -> inserts/updates memory_relations, never enqueues
grep -rn "backfillRelation" internal/ --include="*.go"  -> no match (no relation backfill exists)

💡 Additional Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstatus:approvedApproved for implementation

    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