Summary
When querying an entity with a record-time as_of, asserted facts are correctly rewound to the requested belief state, but derived facts are still returned from the current state.
This can cause a historical belief query to contain derivations that did not exist yet at the requested as_of time.
Tested version
Tested against stock Utopia at:
b03a0f20a3d053323a3ec2669c7c9b4aeb9a8e14
No source, schema, or migration modifications were made.
Reproduction
- Create a knowledge base with entities and asserted facts that allow a rule/inference to produce a derived fact.
- Record the timestamps before those entities/facts are ingested.
- After ingestion and derivation complete, query the entity through MCP
entity_facts using an as_of timestamp from before the entity/facts existed.
- Compare asserted and derived results.
Actual behavior
The asserted facts are correctly absent at the earlier as_of.
However, a derived fact created later is still returned.
In my reproduction, a query at a record-time before the relevant entity/facts had been ingested still returned a later derivation similar to:
Atlas · part of · Acme Systems [rule: transitive]
So the effective result is:
asserted state: rewound by as_of
derived state: current
Expected behavior
A record-time query should represent one internally consistent belief snapshot.
If a derived fact did not exist, or its premises were not present, at the requested as_of, that derived fact should not appear.
Conceptually:
belief_as_of(T)
=
asserted_facts_as_of(T)
+
derived_facts_as_of(T)
rather than:
belief_as_of(T)
=
asserted_facts_as_of(T)
+
derived_facts_now
This matters especially for historical decision review or any other use case that asks:
What did this knowledge base believe at time T?
Relevant code path
At the tested commit, the asserted query receives as_of, while the derived query appears not to:
|
pub async fn entity_facts(ctx: &ToolCtx<'_>, args: &serde_json::Value) -> ToolResult { |
|
let id = args["entity_id"] |
|
.as_str() |
|
.and_then(|s| s.parse::<Uuid>().ok()); |
|
// 世界轴过滤在 SQL 里(world_axis,0022):没起点的事实从最早的证据起,结束了 |
|
// 不知哪天的到说出它的那份文档为止。这里只把 T 传下去,不自己解释 NULL |
|
let at = args["at"].as_str().and_then(parse_when); |
|
// 记录轴(0019 / #347):那一刻**我们持有**的事实。两根轴两个参数,绝不合成 |
|
// 一个——合起来就会拿「三月的世界,以今天的认知」去答「三月的世界,以三月的认知」 |
|
// 「更正到来之前」(#416):`before` 是 changes 里印出来的那个时刻,原样抄过来。 |
|
// 账本的钟是微秒,「严格早于 T」就是「不晚于 T 减一微秒」——这一步在这里做, |
|
// 不让模型对着 ISO 字符串算小数秒的借位:算错一位,答的就是更正**之后**的状态, |
|
// 而且看不出来(#351 那种错)。给了 before 就以它为准 |
|
let before = args["before"].as_str().and_then(parse_when); |
|
let as_of = match before { |
|
Some(t) => Some(just_before(t)), |
|
None => args["as_of"].as_str().and_then(parse_when), |
|
}; |
|
let Some(id) = id else { |
|
return ( |
|
"Invalid entity_id (expected the uuid returned by find_entities).".to_string(), |
|
json!({ "kind": "facts", "label": "?", "detail": "invalid id" }), |
|
); |
|
}; |
|
match utopia_store::graph::entity_detail(&ctx.state.pool, ctx.kb_id, id, at, as_of).await { |
|
Ok((node, facts)) => { |
|
// 规则的结论也是这个实体的一部分(0021)。**不给的话模型会拿那些 |
|
// 读数自己再判一遍**——而阈值写在规则里,它看不见,于是两处判断 |
|
// 迟早不一致,agent 那次还没有前提链、没有区间、也不进账本 |
|
let derived = |
|
utopia_store::reasoning::derived_for_entity(&ctx.state.pool, ctx.kb_id, id, at) |
|
.await |
|
.unwrap_or_default(); |
|
let mut derived: Vec<String> = derived |
|
.iter() |
|
.map(|d| { |
|
format!( |
|
"{} · {} · {} [rule: {}]", |
|
d.subject, |
|
d.predicate, |
|
d.object, |
|
d.rule_name.as_deref().unwrap_or(&d.rule), |
|
) |
|
}) |
|
.collect(); |
|
|
|
let text = if facts.is_empty() && derived.is_empty() { |
|
match at { |
|
Some(t) => format!( |
|
"{}: no facts valid as of {}.", |
|
node.name, |
|
crate::time_text::instant(t) |
|
), |
|
None => format!("{}: no recorded facts.", node.name), |
|
} |
|
} else { |
|
let mut lines: Vec<String> = facts.iter().map(fact_line).collect(); |
|
lines.append(&mut derived); |
|
lines.join("\n") |
|
}; |
|
let detail = entity_facts_detail(facts.len(), at, as_of, before); |
|
( |
|
text, |
|
json!({ "kind": "facts", "label": node.name, "detail": detail }), |
The same semantic question may also apply to derived proof expansion: if a historical derived fact/proof is exposed, its premises and evidence should correspond to the same record-time snapshot.
Suggested acceptance criteria
I am not prescribing a particular implementation, but a regression test could establish the invariant:
- Create asserted facts at record-time
T2.
- Materialize a derivation after
T2.
- Query with
as_of=T1, where T1 < T2.
- Neither the later asserted facts nor derivations based on them are returned.
- Query with current
as_of and both are returned.
- If proof retrieval supports historical queries, the proof and its premises obey the same record-time semantics.
Why I think this is a correctness issue rather than a feature request
Utopia already exposes at and as_of as distinct world-time and record-time semantics. The unexpected behavior is that one category of graph state obeys the record-time axis while another category in the same response does not.
If this behavior is intentional, clarification of the intended semantics would also be useful.
Summary
When querying an entity with a record-time
as_of, asserted facts are correctly rewound to the requested belief state, but derived facts are still returned from the current state.This can cause a historical belief query to contain derivations that did not exist yet at the requested
as_oftime.Tested version
Tested against stock Utopia at:
No source, schema, or migration modifications were made.
Reproduction
entity_factsusing anas_oftimestamp from before the entity/facts existed.Actual behavior
The asserted facts are correctly absent at the earlier
as_of.However, a derived fact created later is still returned.
In my reproduction, a query at a record-time before the relevant entity/facts had been ingested still returned a later derivation similar to:
So the effective result is:
Expected behavior
A record-time query should represent one internally consistent belief snapshot.
If a derived fact did not exist, or its premises were not present, at the requested
as_of, that derived fact should not appear.Conceptually:
rather than:
This matters especially for historical decision review or any other use case that asks:
Relevant code path
At the tested commit, the asserted query receives
as_of, while the derived query appears not to:utopia/crates/utopia-server/src/api/tools.rs
Lines 307 to 370 in b03a0f2
The same semantic question may also apply to derived proof expansion: if a historical derived fact/proof is exposed, its premises and evidence should correspond to the same record-time snapshot.
Suggested acceptance criteria
I am not prescribing a particular implementation, but a regression test could establish the invariant:
T2.T2.as_of=T1, whereT1 < T2.as_ofand both are returned.Why I think this is a correctness issue rather than a feature request
Utopia already exposes
atandas_ofas distinct world-time and record-time semantics. The unexpected behavior is that one category of graph state obeys the record-time axis while another category in the same response does not.If this behavior is intentional, clarification of the intended semantics would also be useful.