Skip to content

migrate: renumbered migration recorded without executing (v102 timeline_entries_source_in_dedup) → all timeline writes fail #2038

@brandobuilds

Description

@brandobuilds

Bug

Our brain reads schema_version 111, but idx_timeline_dedup was still the 3-column (page_id, date, summary) shape. Migration v102 timeline_entries_source_in_dedup — whose own comment says "originally v99, bumped to v102 after master merge" — never executed: the version counter passed it via the pre-renumber chain, so it was recorded as applied without its SQL ever running.

An artifact audit of v101–v109 against the live schema showed only v102 missing (link_kind column, migration_impact_log, atom-hash idx, slug_aliases, extract_rollup_7d, generation clock, embedding_signature, newest_content_at all present).

Consequence at v0.42.x

Every addTimelineEntry / addTimelineEntriesBatch call fails:

there is no unique or exclusion constraint matching the ON CONFLICT specification

because both code paths infer on the 4-column index (page_id, date, summary, source) (src/core/postgres-engine.ts, both insert sites at eefe8b57). On our mesh this silently broke timeline writes on every node, including the hub.

Recovery (what we did)

Ran the migration's own SQL by hand:

DROP INDEX IF EXISTS idx_timeline_dedup;
CREATE UNIQUE INDEX idx_timeline_dedup ON timeline_entries(page_id, date, summary, source);

Suggested fixes

  1. Key applied-migration tracking by stable name, not only the version integer — renumbering during merges then can't strand a migration as recorded-but-not-executed.
  2. A doctor check that verifies migration artifacts (expected index/column/table per migration) rather than trusting the version counter — would have caught this immediately.
  3. Defensively, addTimelineEntry could fall back to a plain insert-where-not-exists when the ON CONFLICT inference fails.

Related observation

The brain_score SQL counts soft-deleted pages in every denominator (SELECT count(*) FROM pages with no deleted_at IS NULL filter), so a brain mid-purge-window is double-penalized on timeline/orphan/link-density components. Possibly intentional, but worth a look while in this code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions