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_fromvalues (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_temporal— CREATEibex_core.memories(not ALTER) - Columns:
valid_from/valid_until/observed_atplus lean content, category, status, tenancy CHECK (valid_until IS NULL OR valid_until > valid_from); half-open interval documented in ADR-0047FORCE ROW LEVEL SECURITY+ibex_core.rls_org_visible; composite(agent_id, org_id)FK;UNIQUE (id, org_id)- Optional
session_idcomposite 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.memorieswithvalid_from,valid_until,observed_at -
valid_fromandobserved_athaveDEFAULT NOW()and areNOT NULL -
valid_untilis nullable - Migration is forward-only in production (down is dev/test only per ADR-0005)
- Migration applies cleanly:
make db-migrate/ integrationUpexits 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 (
000007–000010)
Orientation (historical sketch)
The original sketch used ALTER TABLE. That path was invalid because the table did not exist.
Kept for orientation only:
-- 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
| Column | Meaning | Nullable |
|---|---|---|
valid_from | When the fact described by this memory became true in the real world | No (defaults to NOW() on insert) |
valid_until | When the fact ceased to be true (NULL = still valid) | Yes |
observed_at | When 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.
Last updated on