feat(beliefs): temporal scope and validity; state changes supersede rather than contradict
Beliefs gain a temporal scope (persistent, transient or unknown, classified deterministically from the proposition), an observation time and, for transient states, a validity interval. When a newer belief conflicts with an older transient one observed at least a minute earlier, the older belief is closed (valid_until set, status superseded, a supersedes link, a version and an event saying 'state changed') with its score untouched: 'the door was open at 10:00' stays true of 10:00 when it is closed at 10:05. Simultaneous conflicting reports and persistent propositions contradict as before. Introspection lists superseded states with their validity. Migration 0020 is additive.
This commit is contained in:
@@ -10,6 +10,7 @@ All notable changes to Noema are recorded here. Format follows Keep a Changelog;
|
||||
- Security suite: SQL-injection effect test, static allow-list scan for assembled SQL, deceived-mind authority test, garbage model output test, self-preservation vocabulary test; threat model rewritten so every verification cites a test that asserts the claim.
|
||||
|
||||
### Added
|
||||
- Temporal beliefs: every belief has a temporal scope (persistent, transient, unknown, classified deterministically), an observation time and, for transient states, a validity interval. A later observation that a transient state changed closes the earlier belief (valid_until, superseded, score unchanged) instead of contesting it; simultaneous conflicts and persistent propositions still contradict. Introspection reports what was true and when. Migration 0020.
|
||||
- Confidence semantics: every breakdown states that the number is an uncalibrated epistemic score; belief detail adds a calibrated estimate from the mind's resolved predictions when at least twenty were decided in the same range (otherwise says why not) and names the primary uncertainty deterministically. `docs/beliefs.md` defines every epistemic number.
|
||||
- Explicit `unknown` belief status: no live evidence either way is neither tentative belief nor falsity. Migration 0019.
|
||||
- Assertions: every statement made to a mind is recorded first as an append-only assertion (attributed actor, stance, certainty, modality, negation, reported or quoted speech, hypothetical status, interpretation confidence and notes) before, and separately from, any belief about the proposition. Reported speech is attributed to the person reported and weighed as second-hand; hypotheticals, questions and imperatives create no belief; expressed doubt counts against. API `GET /api/v1/minds/{mind}/assertions`, belief pages show who said so. Migration 0018.
|
||||
|
||||
@@ -72,6 +72,12 @@ Derived from the breakdown unless an operator pinned it:
|
||||
- `rejected` below 0.15; `hypothesis` for hypotheses without support or below 0.5; `tentative` below 0.7; `likely` below 0.85; `accepted` above;
|
||||
- `superseded` when replaced by a newer proposition.
|
||||
|
||||
## Temporal scope
|
||||
|
||||
Every belief has a `temporal_scope`: `persistent` (a fact without an interval: "atlas is the primary host", "Andy prefers Go"), `transient` (a state that can change: "the door is open", "server A is down") or `unknown` when the deterministic classifier (`ClassifyScope`) cannot tell. Transient beliefs record `observed_at` and `valid_from`.
|
||||
|
||||
When a newer belief conflicts with an older **transient** one and was observed at least a minute later, the older belief is not contested and not weakened: its `valid_until` is set to the newer observation, its status becomes `superseded` with a version and a `belief_updated` event saying "state changed", and a `supersedes` link joins them. "The door was open at 10:00" stays true of 10:00 when the door is closed at 10:05. Conflicting reports of the same moment, and conflicts between persistent propositions, are contradictions as described below. Introspection lists superseded states as "Earlier: … (true from … until …)".
|
||||
|
||||
## Contradictions
|
||||
|
||||
On creation and on gaining support, a belief is compared with the mind's live beliefs. The deterministic pass detects the same content with flipped polarity (negation words plus token Jaccard ≥ 0.6). A hit emits `contradiction_detected`, links both beliefs, and makes each counter-evidence for the other weighted by the other's confidence, so both move towards `contested` until more evidence arrives. Paraphrase-level judgement is added by an LLM-assisted process once providers exist; it can only propose, and the same arithmetic applies.
|
||||
|
||||
@@ -36,6 +36,13 @@ type Belief struct {
|
||||
FirstCreated time.Time `json:"first_created"`
|
||||
LastEvaluated time.Time `json:"last_evaluated"`
|
||||
Evaluations int `json:"evaluations"`
|
||||
// Temporal scope and validity (see temporal.go). ValidUntil is set when a
|
||||
// later observation superseded a transient state; the belief then says
|
||||
// what was true, and when.
|
||||
TemporalScope string `json:"temporal_scope"`
|
||||
ObservedAt *time.Time `json:"observed_at,omitempty"`
|
||||
ValidFrom *time.Time `json:"valid_from,omitempty"`
|
||||
ValidUntil *time.Time `json:"valid_until,omitempty"`
|
||||
}
|
||||
|
||||
// EvidenceRow is stored evidence.
|
||||
@@ -125,6 +132,10 @@ type New struct {
|
||||
OriginEvent ids.ID
|
||||
Correlation ids.ID
|
||||
Causation ids.ID
|
||||
// ObservedAt is when the state described was observed (defaults to now);
|
||||
// TemporalScope overrides the deterministic classifier when the producer knows.
|
||||
ObservedAt *time.Time
|
||||
TemporalScope string
|
||||
}
|
||||
|
||||
// EvidenceInput is evidence supplied by callers.
|
||||
@@ -170,14 +181,26 @@ func (s *Store) Create(ctx context.Context, q storage.Querier, emit Emitter, n N
|
||||
prior := PriorFor(n.Epistemic)
|
||||
bd := Compute(prior, ev)
|
||||
st := Derive(bd, n.Epistemic)
|
||||
observed := time.Now()
|
||||
if n.ObservedAt != nil {
|
||||
observed = *n.ObservedAt
|
||||
}
|
||||
scope := n.TemporalScope
|
||||
if scope == "" {
|
||||
scope = ClassifyScope(n.Proposition)
|
||||
}
|
||||
b := Belief{ID: ids.New(), MindID: n.MindID, Proposition: n.Proposition, Subject: strings.TrimSpace(n.Subject), Epistemic: n.Epistemic, Status: st, Confidence: bd.Confidence,
|
||||
Prior: prior, Version: 1, Source: n.Source, Detail: n.Detail, OriginEvent: n.OriginEvent, FirstCreated: time.Now(), LastEvaluated: time.Now(), Evaluations: 1}
|
||||
Prior: prior, Version: 1, Source: n.Source, Detail: n.Detail, OriginEvent: n.OriginEvent, FirstCreated: time.Now(), LastEvaluated: time.Now(), Evaluations: 1,
|
||||
TemporalScope: scope, ObservedAt: &observed}
|
||||
if scope == ScopeTransient {
|
||||
b.ValidFrom = &observed
|
||||
}
|
||||
if b.Detail == nil {
|
||||
b.Detail = map[string]any{}
|
||||
}
|
||||
detail, _ := json.Marshal(b.Detail)
|
||||
if _, err := q.Exec(ctx, `INSERT INTO beliefs (id, mind_id, proposition, subject, epistemic, status, confidence, prior, version, source, detail, origin_event)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,1,$9,$10,$11)`, b.ID, b.MindID, b.Proposition, b.Subject, string(b.Epistemic), string(b.Status), b.Confidence, b.Prior, b.Source, detail, nullID(b.OriginEvent)); err != nil {
|
||||
if _, err := q.Exec(ctx, `INSERT INTO beliefs (id, mind_id, proposition, subject, epistemic, status, confidence, prior, version, source, detail, origin_event, temporal_scope, observed_at, valid_from)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,1,$9,$10,$11,$12,$13,$14)`, b.ID, b.MindID, b.Proposition, b.Subject, string(b.Epistemic), string(b.Status), b.Confidence, b.Prior, b.Source, detail, nullID(b.OriginEvent), b.TemporalScope, b.ObservedAt, b.ValidFrom); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
typ := events.BeliefCreated
|
||||
@@ -274,14 +297,14 @@ func nullID(id ids.ID) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
const cols = `b.id, b.mind_id, b.proposition, b.subject, b.epistemic, b.status, b.confidence, b.prior, b.version, b.source, b.detail, b.origin_event, b.superseded_by, b.pinned_status, b.first_created, b.last_evaluated, b.evaluations`
|
||||
const cols = `b.id, b.mind_id, b.proposition, b.subject, b.epistemic, b.status, b.confidence, b.prior, b.version, b.source, b.detail, b.origin_event, b.superseded_by, b.pinned_status, b.first_created, b.last_evaluated, b.evaluations, b.temporal_scope, b.observed_at, b.valid_from, b.valid_until`
|
||||
|
||||
func scan(row pgx.Row) (Belief, error) {
|
||||
var b Belief
|
||||
var epi, status string
|
||||
var detail []byte
|
||||
var origin, superseded, pinned *string
|
||||
if err := row.Scan(&b.ID, &b.MindID, &b.Proposition, &b.Subject, &epi, &status, &b.Confidence, &b.Prior, &b.Version, &b.Source, &detail, &origin, &superseded, &pinned, &b.FirstCreated, &b.LastEvaluated, &b.Evaluations); err != nil {
|
||||
if err := row.Scan(&b.ID, &b.MindID, &b.Proposition, &b.Subject, &epi, &status, &b.Confidence, &b.Prior, &b.Version, &b.Source, &detail, &origin, &superseded, &pinned, &b.FirstCreated, &b.LastEvaluated, &b.Evaluations, &b.TemporalScope, &b.ObservedAt, &b.ValidFrom, &b.ValidUntil); err != nil {
|
||||
return b, storage.Translate(err)
|
||||
}
|
||||
b.Epistemic, b.Status = concepts.Epistemic(epi), Status(status)
|
||||
@@ -528,6 +551,16 @@ func (s *Store) detectContradictions(ctx context.Context, q storage.Querier, emi
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
if StateChange(o, b) {
|
||||
// A later observation of a transient state: the older belief was
|
||||
// true of its time and is closed, not contested and not weakened.
|
||||
ev, err := s.closeState(ctx, q, emit, o, b, sim, causation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, ev)
|
||||
continue
|
||||
}
|
||||
ev, err := emit(ctx, q, events.New{MindID: b.MindID, Type: events.ContradictionDetected, Source: "beliefs.contradiction", Summary: fmt.Sprintf("contradiction: %q vs %q", trunc(b.Proposition, 60), trunc(o.Proposition, 60)),
|
||||
Salience: 0.8, Confidence: sim, CausationID: causation,
|
||||
Evidence: []events.EvidenceRef{{Kind: "belief", ID: string(b.ID)}, {Kind: "belief", ID: string(o.ID)}},
|
||||
@@ -750,7 +783,7 @@ func (s *Store) Links(ctx context.Context, mind, id ids.ID) ([]Link, error) {
|
||||
var detail []byte
|
||||
var origin, superseded, pinned *string
|
||||
if err := rows.Scan(&l.ID, &l.FromID, &l.ToID, &l.Relation, &l.Confidence, &l.CreatedAt,
|
||||
&other.ID, &other.MindID, &other.Proposition, &other.Subject, &epi, &status, &other.Confidence, &other.Prior, &other.Version, &other.Source, &detail, &origin, &superseded, &pinned, &other.FirstCreated, &other.LastEvaluated, &other.Evaluations); err != nil {
|
||||
&other.ID, &other.MindID, &other.Proposition, &other.Subject, &epi, &status, &other.Confidence, &other.Prior, &other.Version, &other.Source, &detail, &origin, &superseded, &pinned, &other.FirstCreated, &other.LastEvaluated, &other.Evaluations, &other.TemporalScope, &other.ObservedAt, &other.ValidFrom, &other.ValidUntil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
other.Epistemic, other.Status = concepts.Epistemic(epi), Status(status)
|
||||
@@ -876,7 +909,7 @@ func (s *Store) RecentChanges(ctx context.Context, mind ids.ID, since time.Time,
|
||||
var detail []byte
|
||||
var origin, superseded, pinned *string
|
||||
b := &c.Belief
|
||||
if err := rows.Scan(&b.ID, &b.MindID, &b.Proposition, &b.Subject, &epi, &status, &b.Confidence, &b.Prior, &b.Version, &b.Source, &detail, &origin, &superseded, &pinned, &b.FirstCreated, &b.LastEvaluated, &b.Evaluations, &fromStatus, &c.FromConf, &c.Reason, &c.At); err != nil {
|
||||
if err := rows.Scan(&b.ID, &b.MindID, &b.Proposition, &b.Subject, &epi, &status, &b.Confidence, &b.Prior, &b.Version, &b.Source, &detail, &origin, &superseded, &pinned, &b.FirstCreated, &b.LastEvaluated, &b.Evaluations, &b.TemporalScope, &b.ObservedAt, &b.ValidFrom, &b.ValidUntil, &fromStatus, &c.FromConf, &c.Reason, &c.At); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Epistemic, b.Status, c.FromStatus = concepts.Epistemic(epi), Status(status), Status(fromStatus)
|
||||
@@ -906,3 +939,58 @@ func (s *Store) Uncertain(ctx context.Context, mind ids.ID, limit int) ([]Belief
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// closeState ends the validity of an older transient belief when a newer
|
||||
// observation shows the state changed. Its score is untouched; its status
|
||||
// becomes superseded with valid_until set, and a version and an event
|
||||
// record why. "The door was open at 10:00" remains true.
|
||||
func (s *Store) closeState(ctx context.Context, q storage.Querier, emit Emitter, older, newer Belief, sim float64, causation ids.ID) (events.Event, error) {
|
||||
until := *newer.ObservedAt
|
||||
ev, err := emit(ctx, q, events.New{MindID: older.MindID, Type: events.BeliefUpdated, Source: "beliefs.temporal", Summary: fmt.Sprintf("state changed: %q held until %s; now %q", trunc(older.Proposition, 60), until.Format("15:04:05"), trunc(newer.Proposition, 60)),
|
||||
Salience: 0.5, Confidence: sim, CausationID: causation,
|
||||
Evidence: []events.EvidenceRef{{Kind: "belief", ID: string(older.ID), Note: "closed"}, {Kind: "belief", ID: string(newer.ID), Note: "current"}},
|
||||
Metadata: map[string]any{"belief_id": string(older.ID), "superseded_by": string(newer.ID), "valid_until": until, "reason": "state changed"},
|
||||
Payload: map[string]any{"older": map[string]any{"id": older.ID, "proposition": older.Proposition, "valid_from": older.ValidFrom, "valid_until": until}, "newer": map[string]any{"id": newer.ID, "proposition": newer.Proposition, "observed_at": newer.ObservedAt}, "method": "temporal supersession"}})
|
||||
if err != nil {
|
||||
return events.Event{}, err
|
||||
}
|
||||
if _, err := q.Exec(ctx, `UPDATE beliefs SET status='superseded', superseded_by=$3, valid_until=$4, version=version+1, last_evaluated=now(), evaluations=evaluations+1 WHERE id=$1 AND mind_id=$2`, older.ID, older.MindID, newer.ID, until); err != nil {
|
||||
return events.Event{}, err
|
||||
}
|
||||
older.Version++
|
||||
older.Status = StatusSuperseded
|
||||
if err := s.insertVersion(ctx, q, older, Breakdown{Prior: older.Prior, Confidence: older.Confidence, Meaning: ScoreMeaning, Notes: []string{"state changed; score unchanged, validity closed"}}, "state changed: superseded by a later observation", ev.ID); err != nil {
|
||||
return events.Event{}, err
|
||||
}
|
||||
if _, err := q.Exec(ctx, `INSERT INTO belief_links (id, mind_id, from_id, to_id, relation, confidence, event_id) VALUES ($1,$2,$3,$4,'supersedes',$5,$6) ON CONFLICT DO NOTHING`, ids.New(), older.MindID, newer.ID, older.ID, sim, ev.ID); err != nil {
|
||||
return events.Event{}, err
|
||||
}
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// History lists superseded beliefs about a topic with their validity, so a
|
||||
// mind can say what was true and when.
|
||||
func (s *Store) History(ctx context.Context, mind ids.ID, text string, limit int) ([]Belief, error) {
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 10
|
||||
}
|
||||
toks, _ := Polarity(text)
|
||||
if len(toks) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
q := strings.Join(toks, " | ")
|
||||
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status='superseded' AND b.valid_until IS NOT NULL AND b.search @@ to_tsquery('english', $2) ORDER BY b.valid_until DESC LIMIT $3`, mind, q, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Belief
|
||||
for rows.Next() {
|
||||
b, err := scan(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, b)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
evstore "github.com/andydixon/noema/internal/events"
|
||||
"github.com/andydixon/noema/internal/minds"
|
||||
@@ -161,3 +162,70 @@ func TestUnknownIsNotFalse(t *testing.T) {
|
||||
t.Fatal("absence of evidence must not read as falsity")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransientStatesSupersedeInsteadOfContesting: a later observation of a
|
||||
// changed state closes the earlier belief's validity and leaves it true of
|
||||
// its time; persistent propositions still contradict.
|
||||
func TestTransientStatesSupersedeInsteadOfContesting(t *testing.T) {
|
||||
if ClassifyScope("the door is open") != ScopeTransient || ClassifyScope("server-a is down") != ScopeTransient || ClassifyScope("atlas is the primary database host") != ScopePersistent || ClassifyScope("andy prefers go") != ScopePersistent {
|
||||
t.Fatal("scope classifier")
|
||||
}
|
||||
pool := storage.TestPool(t, migrations.FS)
|
||||
ctx := context.Background()
|
||||
m, _ := minds.NewStore(pool).Create(ctx, minds.CreateParams{Slug: "tmp"})
|
||||
es := evstore.NewStore(pool, evstore.NewBus(8), nil)
|
||||
emit := func(ctx context.Context, q storage.Querier, n events.New) (events.Event, error) {
|
||||
out, err := es.AppendIn(ctx, q, n)
|
||||
if err != nil {
|
||||
return events.Event{}, err
|
||||
}
|
||||
return out[0], nil
|
||||
}
|
||||
s := NewStore(pool)
|
||||
ten := time.Date(2026, 9, 15, 10, 0, 0, 0, time.UTC)
|
||||
tenFive := ten.Add(5 * time.Minute)
|
||||
ev := []EvidenceInput{{Direction: "for", Kind: "note", Source: "sensor", Weight: 0.7, Credibility: 0.9}}
|
||||
open, err := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the door is open", Subject: "door", Epistemic: concepts.Observation, Source: "sensor", Evidence: ev, ObservedAt: &ten})
|
||||
if err != nil || open.Belief.TemporalScope != ScopeTransient || open.Belief.ValidFrom == nil {
|
||||
t.Fatalf("open: %+v %v", open.Belief, err)
|
||||
}
|
||||
closed, err := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the door is not open", Subject: "door", Epistemic: concepts.Observation, Source: "sensor", Evidence: ev, ObservedAt: &tenFive})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, _ := s.Get(ctx, pool, m.ID, open.Belief.ID)
|
||||
if before.Status != StatusSuperseded || before.ValidUntil == nil || !before.ValidUntil.Equal(tenFive) || before.SupersededBy != closed.Belief.ID {
|
||||
t.Fatalf("earlier state should be closed, not contested: %+v", before)
|
||||
}
|
||||
if before.Confidence != open.Belief.Confidence {
|
||||
t.Fatalf("closing a state must not change its score: %.3f -> %.3f", open.Belief.Confidence, before.Confidence)
|
||||
}
|
||||
now, _ := s.Get(ctx, pool, m.ID, closed.Belief.ID)
|
||||
if now.Status == StatusContested || now.Confidence < closed.Belief.Confidence {
|
||||
t.Fatalf("current state should not be contested: %+v", now)
|
||||
}
|
||||
if rows, _ := s.EvidenceFor(ctx, pool, m.ID, now.ID); len(rows) != 1 {
|
||||
t.Fatalf("no counter-evidence should be written for a state change: %+v", rows)
|
||||
}
|
||||
hist, _ := s.History(ctx, m.ID, "door open", 5)
|
||||
if len(hist) != 1 || hist[0].ID != open.Belief.ID {
|
||||
t.Fatalf("history: %+v", hist)
|
||||
}
|
||||
// Same-moment conflicting reports remain a contradiction.
|
||||
a, _ := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the pump is running", Subject: "pump", Epistemic: concepts.Observation, Source: "a", Evidence: ev, ObservedAt: &ten})
|
||||
b, _ := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "the pump is not running", Subject: "pump", Epistemic: concepts.Observation, Source: "b", Evidence: ev, ObservedAt: &ten})
|
||||
ab, _ := s.Get(ctx, pool, m.ID, a.Belief.ID)
|
||||
if ab.Status == StatusSuperseded || ab.ValidUntil != nil {
|
||||
t.Fatalf("simultaneous conflict must contest, not supersede: %+v", ab)
|
||||
}
|
||||
if links, _ := s.Links(ctx, m.ID, b.Belief.ID); len(links) != 1 || links[0].Relation != "contradicts" {
|
||||
t.Fatalf("contradiction link expected: %+v", links)
|
||||
}
|
||||
// Persistent propositions contradict regardless of time.
|
||||
p1, _ := s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "atlas is the primary database host", Subject: "atlas", Epistemic: concepts.Claim, Source: "x", Evidence: ev, ObservedAt: &ten})
|
||||
s.Create(ctx, pool, emit, New{MindID: m.ID, Proposition: "atlas is not the primary database host", Subject: "atlas", Epistemic: concepts.Claim, Source: "y", Evidence: ev, ObservedAt: &tenFive}) //nolint:errcheck
|
||||
p1b, _ := s.Get(ctx, pool, m.ID, p1.Belief.ID)
|
||||
if p1b.Status == StatusSuperseded {
|
||||
t.Fatalf("persistent fact wrongly treated as a state change: %+v", p1b)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package beliefs
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Temporal scopes. Persistent propositions are true or false without an
|
||||
// interval ("atlas is the primary host", "Andy prefers Go"). Transient ones
|
||||
// describe a state that can change ("the door is open", "server A is down")
|
||||
// and carry a validity interval. Unknown is the honest default when the
|
||||
// classifier cannot tell.
|
||||
const (
|
||||
ScopePersistent = "persistent"
|
||||
ScopeTransient = "transient"
|
||||
ScopeUnknown = "unknown"
|
||||
)
|
||||
|
||||
// MinStateChangeGap is the shortest interval between two observations for a
|
||||
// later, conflicting observation of a transient state to count as a state
|
||||
// change rather than a contradiction. Two reports of the same moment that
|
||||
// disagree are a contradiction; reports minutes apart are a change.
|
||||
const MinStateChangeGap = time.Minute
|
||||
|
||||
var (
|
||||
reTransient = regexp.MustCompile(`(?i)\b(?:is|are|was|were|remains|seems|looks|appears|has been|have been|is currently|is now)\s+(?:not\s+|still\s+|no longer\s+)?(?:down|up|open|closed|offline|online|running|stopped|stuck|failing|degraded|full|empty|busy|idle|locked|unlocked|available|unavailable|slow|broken|healthy|unhealthy|overloaded|unreachable|reachable|responding|paused|active|inactive|pending|in progress|at capacity|on call|away|present|absent|late|early|hot|cold|warm|crossed)\b|\b(?:right now|currently|at the moment|at \d{1,2}:\d{2}|between \d|since \d|until \d|today|tonight|this morning|this afternoon|for the last|in the last)\b`)
|
||||
rePersistent = regexp.MustCompile(`(?i)\b(?:is (?:a|an|the)\b|are (?:a|an|the)\b|was born|prefers?|likes?|dislikes?|always|never|is named|is called|is the name|belongs to|owns|is made of|is located in|is based in|works at|works for|is part of|means|stands for|is written in|uses|runs on|depends on)\b`)
|
||||
)
|
||||
|
||||
// ClassifyScope decides, deterministically, whether a proposition names a
|
||||
// transient state, a persistent fact, or neither recognisably.
|
||||
func ClassifyScope(proposition string) string {
|
||||
p := strings.TrimSpace(proposition)
|
||||
switch {
|
||||
case reTransient.MatchString(p):
|
||||
return ScopeTransient
|
||||
case rePersistent.MatchString(p):
|
||||
return ScopePersistent
|
||||
}
|
||||
return ScopeUnknown
|
||||
}
|
||||
|
||||
// StateChange reports whether a newer belief should close an older,
|
||||
// conflicting one as a change of state rather than contest it: both must be
|
||||
// transient, and the newer observation must be later by at least the gap.
|
||||
func StateChange(older, newer Belief) bool {
|
||||
if older.TemporalScope != ScopeTransient || newer.TemporalScope != ScopeTransient {
|
||||
return false
|
||||
}
|
||||
if older.ObservedAt == nil || newer.ObservedAt == nil {
|
||||
return false
|
||||
}
|
||||
return newer.ObservedAt.Sub(*older.ObservedAt) >= MinStateChangeGap
|
||||
}
|
||||
@@ -123,7 +123,8 @@ func (h Handlers) perceive(ctx context.Context, c *brain.Cycle, t workspace.Thou
|
||||
Note: actor + " doubts it", Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(actor), RootIDs: []string{srcID}})
|
||||
}
|
||||
var err error
|
||||
res, err = h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: in.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID, Evidence: ev,
|
||||
observed := c.State.Now
|
||||
res, err = h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: in.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID, Evidence: ev, ObservedAt: &observed,
|
||||
Detail: map[string]any{"attributed_to": attributed, "reported": in.Reported, "interpretation_confidence": in.Confidence}})
|
||||
if err != nil {
|
||||
c.Emit(events.New{Type: events.Metacognition, Source: "brain.perceive", Summary: "claim not recorded: " + err.Error(), Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "claim_rejected"}})
|
||||
@@ -304,6 +305,17 @@ func (h Handlers) addKnowledge(ctx context.Context, c *brain.Cycle, in *Intent,
|
||||
in.Uncertainty = fmt.Sprintf("The strongest relevant belief is only at %.2f.", maxConf)
|
||||
}
|
||||
}
|
||||
// Historical truth is not overwritten by the present: say what was
|
||||
// true and when, alongside what is believed now.
|
||||
if past, _ := h.Beliefs.History(ctx, mind, topic, 3); len(past) > 0 {
|
||||
for _, b := range past {
|
||||
from := "earlier"
|
||||
if b.ValidFrom != nil {
|
||||
from = b.ValidFrom.Format("15:04 on 2 Jan")
|
||||
}
|
||||
in.Points = append(in.Points, fmt.Sprintf("Earlier: %s (true from %s until %s, then superseded).", b.Proposition, from, b.ValidUntil.Format("15:04 on 2 Jan")))
|
||||
}
|
||||
}
|
||||
}
|
||||
if h.Memory != nil {
|
||||
if hits, _ := h.Memory.Retrieve(ctx, mind, memory.Query{Text: topic, Limit: 4, Now: c.State.Now, Semantic: true}); len(hits) > 0 {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
DROP INDEX IF EXISTS beliefs_mind_subject_validity_idx;
|
||||
ALTER TABLE beliefs
|
||||
DROP COLUMN IF EXISTS valid_until,
|
||||
DROP COLUMN IF EXISTS valid_from,
|
||||
DROP COLUMN IF EXISTS observed_at,
|
||||
DROP COLUMN IF EXISTS temporal_scope;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Temporal scope (review pass §7.2 item 12). A belief may describe a
|
||||
-- persistent fact or a transient state. Transient states have a validity
|
||||
-- interval: a later observation that the state changed closes the interval
|
||||
-- and supersedes the belief instead of contesting it, so what was true at
|
||||
-- 10:00 stays true of 10:00 when 10:05 differs.
|
||||
ALTER TABLE beliefs
|
||||
ADD COLUMN IF NOT EXISTS temporal_scope text NOT NULL DEFAULT 'unknown' CHECK (temporal_scope IN ('persistent','transient','unknown')),
|
||||
ADD COLUMN IF NOT EXISTS observed_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS valid_from timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS valid_until timestamptz;
|
||||
CREATE INDEX IF NOT EXISTS beliefs_mind_subject_validity_idx ON beliefs (mind_id, subject, valid_until);
|
||||
@@ -6,6 +6,7 @@
|
||||
<p class="score-line"><strong>Epistemic score {{conf $b.Confidence}}</strong> <span class="muted small">(uncalibrated: derived from recorded evidence, not a probability of truth)</span>
|
||||
· calibrated: {{if $d.Calibration.Available}}<strong>{{conf $d.Calibration.Estimate}}</strong> <span class="muted small">(hit rate of {{$d.Calibration.SampleSize}} decided predictions in this range)</span>{{else}}<span class="muted small">not available ({{$d.Calibration.Reason}})</span>{{end}}
|
||||
· main uncertainty: <span class="muted">{{$d.PrimaryUncertainty}}</span></p>
|
||||
<p class="muted small">temporal scope <span class="badge small">{{$b.TemporalScope}}</span>{{with $b.ObservedAt}} · observed {{date .}}{{end}}{{with $b.ValidFrom}} · valid from {{date .}}{{end}}{{with $b.ValidUntil}} · <strong>until {{date .}}</strong> (superseded by a later observation; this remained true of its time){{end}}</p>
|
||||
<p class="muted">{{$b.Epistemic}} · version {{$b.Version}} · first {{date $b.FirstCreated}} · evaluated {{$b.Evaluations}}× · source {{$b.Source}}{{if $b.Subject}} · subject <span class="mono">{{$b.Subject}}</span>{{end}}{{if $b.PinnedStatus}} · <span class="badge warn">status pinned by operator</span>{{end}}</p>
|
||||
</div>
|
||||
<section class="grid cols-3-1">
|
||||
|
||||
Reference in New Issue
Block a user