Skip to content

fix(sync): include memory_relations in local chunk sync#489

Merged
Alan-TheGentleman merged 2 commits into
mainfrom
fix/local-sync-relations
Jun 13, 2026
Merged

fix(sync): include memory_relations in local chunk sync#489
Alan-TheGentleman merged 2 commits into
mainfrom
fix/local-sync-relations

Conversation

@Alan-TheGentleman

@Alan-TheGentleman Alan-TheGentleman commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #353

Type

  • Bug fix

Summary

  • Local engram sync silently omitted memory_relations from .engram/chunks/*.jsonl.gz, dropping the relation graph on import and violating the documented "no silent drops" invariant.
  • Adds ExportRelationMutations to query non-orphaned relations per project and append them as explicit relation upsert mutations to the chunk; import restores them idempotently via the existing applyRelationUpsertTx, with observation upserts ordered before relation upserts so FK preconditions hold.
  • The export call runs only in local (non-cloud) mode and propagates errors loudly.

Changes

File Change
internal/store/store.go New ExportRelationMutations — queries non-orphaned relations for a project with JOIN-verified FK integrity
internal/sync/sync.go Local export path appends relation mutations to the chunk; gated to non-cloud mode; loud error propagation; IsEmpty accounts for mutation-only chunks
internal/store/relations_test.go Direct tests: project-scoped, unscoped, orphaned-excluded
internal/sync/sync_test.go Round-trip, idempotent re-import, backward-compat old chunks, project isolation, incremental export, export-relations error path
docs/codebase/sync-and-cloud.md Documents relations in the local chunk payload
DOCS.md Aligned reference

Test Plan

  • go build ./... passes
  • go vet ./internal/... passes
  • go test ./internal/sync/... ./internal/store/... passes (sync 89.9%, store 78.7% coverage)
  • Lossless round-trip verified: export then import restores the relation graph
  • Backward compatible with old relation-free chunks
  • Dual adversarial review (Judgment Day) — APPROVED after fixing test-coverage gaps

Contributor Checklist

  • Linked an approved issue (status:approved)
  • Added exactly one type:* label
  • Docs updated (behavior changed)
  • Conventional commit format
  • No Co-Authored-By trailers

Summary by CodeRabbit

  • New Features

    • Memory relations are now exported and imported during sync operations, including support for project-scoped and full exports with proper filtering of orphaned relations.
  • Documentation

    • Updated documentation to clarify that project-scoped sync chunks now include relations data alongside sessions, observations, and prompts.
  • Tests

    • Added comprehensive test coverage for relation export and import functionality in sync operations.

Local 'engram sync' wrote chunks containing only sessions, observations,
and prompts, silently omitting memory_relations rows. On import the
relation graph was lost, violating the 'no silent drops' invariant.

Add ExportRelationMutations to query non-orphaned relations per project
and append them as explicit relation upsert mutations to the chunk. On
import the existing applyRelationUpsertTx restores them idempotently,
with observation upserts ordered before relation upserts so FK
preconditions hold. The export call runs only in local (non-cloud)
mode and propagates errors loudly.

Closes #353
Copilot AI review requested due to automatic review settings June 13, 2026 10:30
@Alan-TheGentleman Alan-TheGentleman added the type:bug Bug fix label Jun 13, 2026
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extends local chunk sync to include memory_relations in exported chunks, resolving issue #353. It adds store-level relation export, integrates relation mutations into the sync chunk path with incremental filtering, tests the full export/import cycle, and updates documentation.

Changes

Local chunk sync includes memory_relations via relation mutations

Layer / File(s) Summary
Store-level relation mutation export
internal/store/store.go, internal/store/relations_test.go
Adds ExportRelationMutations to query non-orphaned relations and return SyncMutation entries for each, with optional project filtering and ordering by creation time. Tests cover project-scoped, unscoped, and orphan-exclusion behaviors.
Sync export/import with relation mutations
internal/sync/sync.go
Integrates relation export into chunk export flow: wires storeExportRelations hook, computes manifest cutoff timestamp, exports relations from store, and filters both base entities and relation mutations by OccurredAt for incremental exports. Adds filterNewRelationMutations helper for timestamp-based filtering.
Sync test coverage for relation export and import
internal/sync/sync_test.go
Introduces test helpers for seeding relations with project scoping and session inheritance. Adds seven test cases covering local chunk export/import, project exclusion, incremental behavior, legacy chunk compatibility, and relation restoration. Updates hook management and adds error-handling subtest.
Documentation updates
docs/codebase/sync-and-cloud.md, DOCS.md
Clarifies that project-scoped chunks include non-orphaned memory_relations via relation sync mutations for idempotent import behavior. Adjusts schema table line placement.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding memory_relations support to local chunk sync, which directly addresses the core objective of fixing the silent drop bug.
Linked Issues check ✅ Passed The PR fully implements the primary objective from issue #353: including memory_relations in local chunks via ExportRelationMutations, proper filtering, and idempotent import paths, satisfying the core requirement to fix the silent drop bug.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the issue #353 requirements: relation export logic, filtering, tests, documentation updates, and sync integration—no unrelated modifications present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/local-sync-relations

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/store/store.go`:
- Around line 3378-3423: ExportRelationMutations currently only enforces
source/target project parity when normalizedProject is provided, causing
unscoped exports to inherit the source project even for cross-project relations;
update the query construction in ExportRelationMutations (the query string,
normalizedProject conditional, and args slice) so that when normalizedProject ==
"" you still add a WHERE clause enforcing coalesce(nullif(src.project,''),
src_s.project,'') = coalesce(nullif(tgt.project,''), tgt_s.project,'') (and keep
the SELECT column for Project as-is), and when normalizedProject != "" keep the
existing equality checks; ensure the args slice is updated only when needed and
that the parity check is included in the final query before executing
s.queryItHook.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0156b14b-2fe2-4102-a1c8-899f3ed399ac

📥 Commits

Reviewing files that changed from the base of the PR and between 68a19f8 and f4cc070.

📒 Files selected for processing (6)
  • DOCS.md
  • docs/codebase/sync-and-cloud.md
  • internal/store/relations_test.go
  • internal/store/store.go
  • internal/sync/sync.go
  • internal/sync/sync_test.go

Comment thread internal/store/store.go
Comment on lines +3378 to +3423
query := `
SELECT r.sync_id, r.source_id, r.target_id, r.relation, r.reason, r.evidence, r.confidence,
r.judgment_status, r.marked_by_actor, r.marked_by_kind, r.marked_by_model,
r.session_id, coalesce(nullif(src.project, ''), src_s.project, ''), r.created_at, r.updated_at
FROM memory_relations r
JOIN observations src ON src.sync_id = r.source_id AND src.deleted_at IS NULL
JOIN observations tgt ON tgt.sync_id = r.target_id AND tgt.deleted_at IS NULL
LEFT JOIN sessions src_s ON src_s.id = src.session_id
LEFT JOIN sessions tgt_s ON tgt_s.id = tgt.session_id
WHERE r.judgment_status != ?`
args := []any{JudgmentStatusOrphaned}
if normalizedProject != "" {
query += ` AND coalesce(nullif(src.project, ''), src_s.project, '') = ?
AND coalesce(nullif(tgt.project, ''), tgt_s.project, '') = ?`
args = append(args, normalizedProject, normalizedProject)
}
query += ` ORDER BY r.created_at, r.sync_id`

rows, err := s.queryItHook(s.db, query, args...)
if err != nil {
return nil, fmt.Errorf("export relation mutations: %w", err)
}
defer rows.Close()

mutations := []SyncMutation{}
for rows.Next() {
var p syncRelationPayload
if err := rows.Scan(
&p.SyncID, &p.SourceID, &p.TargetID, &p.Relation, &p.Reason, &p.Evidence, &p.Confidence,
&p.JudgmentStatus, &p.MarkedByActor, &p.MarkedByKind, &p.MarkedByModel,
&p.SessionID, &p.Project, &p.CreatedAt, &p.UpdatedAt,
); err != nil {
return nil, fmt.Errorf("export relation mutations: scan: %w", err)
}
payload, err := json.Marshal(p)
if err != nil {
return nil, fmt.Errorf("export relation mutations: marshal %s: %w", p.SyncID, err)
}
mutations = append(mutations, SyncMutation{
Entity: SyncEntityRelation,
EntityKey: strings.TrimSpace(p.SyncID),
Op: SyncOpUpsert,
Payload: string(payload),
Project: strings.TrimSpace(p.Project),
OccurredAt: p.UpdatedAt,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce source/target project parity in unscoped relation export.

ExportRelationMutations only enforces project equality when a project filter is provided. In the unscoped path, Project is derived from the source side only, so a cross-project relation row would be exported with mislabeled project metadata and can break downstream project-scoped sync semantics.

Suggested fix
-	query := `
-		SELECT r.sync_id, r.source_id, r.target_id, r.relation, r.reason, r.evidence, r.confidence,
-		       r.judgment_status, r.marked_by_actor, r.marked_by_kind, r.marked_by_model,
-		       r.session_id, coalesce(nullif(src.project, ''), src_s.project, ''), r.created_at, r.updated_at
+	query := `
+		SELECT r.sync_id, r.source_id, r.target_id, r.relation, r.reason, r.evidence, r.confidence,
+		       r.judgment_status, r.marked_by_actor, r.marked_by_kind, r.marked_by_model,
+		       r.session_id, coalesce(nullif(src.project, ''), src_s.project, ''), r.created_at, r.updated_at
 		FROM memory_relations r
 		JOIN observations src ON src.sync_id = r.source_id AND src.deleted_at IS NULL
 		JOIN observations tgt ON tgt.sync_id = r.target_id AND tgt.deleted_at IS NULL
 		LEFT JOIN sessions src_s ON src_s.id = src.session_id
 		LEFT JOIN sessions tgt_s ON tgt_s.id = tgt.session_id
-		WHERE r.judgment_status != ?`
+		WHERE r.judgment_status != ?
+		  AND coalesce(nullif(src.project, ''), src_s.project, '') =
+		      coalesce(nullif(tgt.project, ''), tgt_s.project, '')`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/store.go` around lines 3378 - 3423, ExportRelationMutations
currently only enforces source/target project parity when normalizedProject is
provided, causing unscoped exports to inherit the source project even for
cross-project relations; update the query construction in
ExportRelationMutations (the query string, normalizedProject conditional, and
args slice) so that when normalizedProject == "" you still add a WHERE clause
enforcing coalesce(nullif(src.project,''), src_s.project,'') =
coalesce(nullif(tgt.project,''), tgt_s.project,'') (and keep the SELECT column
for Project as-is), and when normalizedProject != "" keep the existing equality
checks; ensure the args slice is updated only when needed and that the parity
check is included in the final query before executing s.queryItHook.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes local (non-cloud) engram sync exports that previously omitted memory_relations, causing relation graphs to be dropped on import and violating the “no silent drops” invariant.

Changes:

  • Add Store.ExportRelationMutations(project) to export non-orphaned relation rows as explicit relation upsert sync mutations.
  • Extend local chunk export to append relation mutations (and treat mutation-only chunks as non-empty); import reuses the existing mutation apply pipeline with ordering that keeps FK prerequisites intact.
  • Add focused store + sync round-trip/idempotency/backward-compat tests and update docs to reflect relations being included in local chunks.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/store/store.go Adds ExportRelationMutations SQL export of relation upsert mutations scoped by project.
internal/sync/sync.go Local export now includes relation mutations in chunk payload; incremental filtering for relation mutations.
internal/store/relations_test.go Unit tests for relation mutation export: project-scoped, unscoped, orphan-excluded.
internal/sync/sync_test.go End-to-end tests for local chunk export/import with relations, idempotency, backward compatibility, isolation, and error propagation.
docs/codebase/sync-and-cloud.md Documents that local project chunks include relations via sync mutations.
DOCS.md Updates DB/reference docs to reflect relations syncing via local chunks as well as cloud.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/store/store.go
JOIN observations tgt ON tgt.sync_id = r.target_id AND tgt.deleted_at IS NULL
LEFT JOIN sessions src_s ON src_s.id = src.session_id
LEFT JOIN sessions tgt_s ON tgt_s.id = tgt.session_id
WHERE r.judgment_status != ?`
@Alan-TheGentleman Alan-TheGentleman merged commit d38bbab into main Jun 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(sync): local chunk sync silently drops memory_relations without warning

2 participants