knowledge: valid-time on facts + interval-aware conflict detection
Adds bitemporal support to entity_facts. Existing `created_at` is
transaction time (when we recorded the fact); the new
`valid_from` / `valid_until` BIGINT columns are valid time (when the
fact is/was true in the real world). NULL on either side = unbounded
on that side, both NULL = "always-true / unknown" — matches the
default state of every legacy row, no backfill needed.
The split matters for time-bounded predicates like
is_in_relationship_with / lives_in / works_at: recording the fact
once doesn't mean the relationship is still ongoing. Same predicate
across different windows ("lives_in NYC 2018-2020", "lives_in SF
2020-present") is no longer a conflict — the interval-aware check
in get_entity only flags pairs whose windows overlap. Facts with no
valid-time data still flag against everything (worst case for legacy
rows — user adds dates to suppress).
API surface:
- POST /knowledge/facts accepts optional valid_from / valid_until.
- PATCH /knowledge/facts/{id} accepts both with tri-state semantics:
field omitted = leave alone, JSON null = clear to NULL, number =
set. Implemented via a small serde helper around Option<Option>.
- GET /knowledge/entities/{id} surfaces both fields per fact and
uses them in conflict detection.
Agent path (insight_generator) writes NULL/NULL for now — deriving
valid_from from the source photo's date_taken is slated for a
follow-up agent tool alongside Phase 2's supersession.
Test pins set + clear semantics via update_fact: setting both
bounds, leaving them alone on a subsequent patch, then clearing
valid_until back to NULL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -249,6 +249,15 @@ pub struct InsertEntityFact {
|
||||
/// persona must not see each other's facts. Always paired with
|
||||
/// `persona_id` — they're a unit.
|
||||
pub user_id: i32,
|
||||
/// Real-world period the fact is/was true (unix seconds). NULL on
|
||||
/// either side = unbounded — `valid_from IS NULL` reads as
|
||||
/// "always-true-back-to-the-beginning", `valid_until IS NULL` as
|
||||
/// "still-true-now-or-unknown". Distinguishes valid time from
|
||||
/// transaction time (`created_at` is when we recorded the fact,
|
||||
/// not when it was true in the world). See migration
|
||||
/// 2026-05-10-000100.
|
||||
pub valid_from: Option<i64>,
|
||||
pub valid_until: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||||
@@ -265,6 +274,8 @@ pub struct EntityFact {
|
||||
pub created_at: i64,
|
||||
pub persona_id: String,
|
||||
pub user_id: i32,
|
||||
pub valid_from: Option<i64>,
|
||||
pub valid_until: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
|
||||
Reference in New Issue
Block a user