Phase 2.5 provider generalization

Foundation ibex_core.memories with valid_from, valid_until, and observed_at — bi-temporal validity for Phase 3 conflict detection, shipped as CREATE (table did not exist) with FORCE RLS.

Milestone 2.5.G5.M1 — Temporal Validity Columns

Status: Completed (2026-08-24)
Goal: Track E — Schema Pre-Work
Phase: 2.5 — Provider Generalization & Foundation
Estimated effort: 0.5 days
ADR: ADR-0047


Why This Milestone Exists

The schema tracked memory lifecycle status (active/superseded/merged_into/archived/quarantined/deleted) but had no notion of when a fact was true, which is exactly the gap Zep/Graphiti-style temporal graphs close.

This is the schema-level fix for the "contradiction requires a similarity threshold to even detect" problem — a contradiction becomes two rows with overlapping [valid_from, valid_until) ranges and a contradicts edge in memory_relationships, rather than requiring the extraction worker to silently pick a winner.

This milestone is migration-only, additive, and unused by application code yet — the point is to have the right shape in Postgres before Phase 3's write pipeline is built against it.


Non-Goals

  • Application code reading or writing the new columns (Phase 3)
  • Temporal query patterns or indexes (Phase 3)
  • Backfilling historical valid_from values (no prior rows)
  • Embedding / pgvector / HNSW (Phase 3.1.1 expand)
  • memory_categories / memory_relationships (G5.M2 / G5.M3)

What shipped

  • Migration 000014_create_memories_temporalCREATE ibex_core.memories (not ALTER)
  • Columns: valid_from / valid_until / observed_at plus lean content, category, status, tenancy
  • CHECK (valid_until IS NULL OR valid_until > valid_from); half-open interval documented in ADR-0047
  • FORCE ROW LEVEL SECURITY + ibex_core.rls_org_visible; composite (agent_id, org_id) FK; UNIQUE (id, org_id)
  • Optional session_id composite FK; clear via trigger on session delete
  • Integration tests: columns/defaults/CHECK/RLS/idempotent up
  • Docs: ADR-0047, DATABASE_SCHEMA.md, 3.1.1 expand-not-CREATE callout

Success signals

  • Migration creates ibex_core.memories with valid_from, valid_until, observed_at
  • valid_from and observed_at have DEFAULT NOW() and are NOT NULL
  • valid_until is nullable
  • Migration is forward-only in production (down is dev/test only per ADR-0005)
  • Migration applies cleanly: make db-migrate / integration Up exits 0
  • No application code reads or writes the new columns in this milestone
  • Consistent with expand-contract (3.1.1 expands; does not double-CREATE)

Prerequisites

  • Track D (embedder) merged — sequencing only; no code dependency
  • Sessions / agents / RLS helpers from Phase 2 migrations (000007000010)

Orientation (historical sketch)

The original sketch used ALTER TABLE. That path was invalid because the table did not exist. Kept for orientation only:

SQL
-- Sketch only — do not apply; shipped path is CREATE in 000014
ALTER TABLE ibex_core.memories
 ADD COLUMN valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
 ADD COLUMN valid_until TIMESTAMPTZ,
 ADD COLUMN observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW();

Column semantics

ColumnMeaningNullable
valid_fromWhen the fact described by this memory became true in the real worldNo (defaults to NOW() on insert)
valid_untilWhen the fact ceased to be true (NULL = still valid)Yes
observed_atWhen IBEX learned this fact (distinct from when it became true)No (defaults to NOW() on insert)

Why observed_at is distinct from valid_from: A user might tell IBEX "I moved to Berlin three years ago" — the fact (valid_from = 3 years ago) and the observation (observed_at = now) are different instants.

Edit on GitHub

Last updated on

On this page

0%