Files
noema/internal/cognition/associate.go
T
Andy Dixon 0b790f34c7 fix: let fresh causal hypotheses compete with fading recalls
Association candidates scored below decayed workspace residents and were
always rejected in a small workspace. Raise their novelty, consequence
and operator-priority features so a new hypothesis outranks a stale
recall; confidence of the resulting belief is unchanged (still computed
from the two co-occurring observations).
2026-06-23 20:37:01 +01:00

77 lines
2.7 KiB
Go

package cognition
import (
"context"
"sort"
"strings"
"time"
"github.com/andydixon/noema/internal/attention"
"github.com/andydixon/noema/internal/brain"
"github.com/andydixon/noema/internal/workspace"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
)
// Associate proposes low-confidence causal hypotheses when two notable
// observations arrive close together ("deployment at 09:10, latency at
// 09:12"). It is deliberately weak: confidence comes from the belief layer's
// arithmetic over the two observations, never from the wording.
type Associate struct {
Window time.Duration // how far back an earlier observation may lie
}
// Name implements brain.Process.
func (Associate) Name() string { return "associate" }
// Observe implements brain.Process.
func (a Associate) Observe(_ context.Context, s *brain.State) ([]brain.Candidate, error) {
if s.Services.Beliefs == nil {
return nil, nil
}
window := a.Window
if window <= 0 {
window = 15 * time.Minute
}
var out []brain.Candidate
for _, ev := range s.NewEvents {
if strings.HasPrefix(ev.Source, "brain") || ev.Type != events.Observation {
continue
}
laterToks := workspace.Tokenise(ev.Summary)
laterU := urgency(laterToks)
if laterU < 0.4 {
continue
}
for _, it := range s.Workspace.WorkingMemory {
if it.Kind != "observation" || it.SourceEvent.IsZero() || it.SourceEvent == ev.ID {
continue
}
if ev.OccurredAt.Sub(it.TouchedAt) > window || it.TouchedAt.After(ev.OccurredAt) {
continue
}
earlierToks := workspace.Tokenise(it.Content)
if urgency(earlierToks) < 0.3 && workspace.Overlap(earlierToks, toSet(laterToks)) < 0.2 {
continue
}
pair := []string{string(it.SourceEvent), string(ev.ID)}
sort.Strings(pair)
prop := truncate(it.Content, 100) + " may have caused: " + truncate(ev.Summary, 100)
out = append(out, brain.Candidate{Kind: "hypothesis", Subject: strings.Join(pair, "+"), Summary: "hypothesis: " + prop,
Content: map[string]any{"proposition": prop, "cause_event": string(it.SourceEvent), "effect_event": string(ev.ID), "gap_seconds": int(ev.OccurredAt.Sub(it.TouchedAt).Seconds()), "source_event": string(ev.ID)},
Features: attention.Features{Salience: 0.4 + 0.5*ev.Salience, Novelty: 0.85, Consequence: 0.8, Urgency: laterU * 0.6, Confidence: 0.3, ContextRelevance: 0.6, OperatorPriority: 0.5},
Confidence: 0.3, SourceEvents: []ids.ID{ev.ID, it.SourceEvent},
Evidence: []events.EvidenceRef{{Kind: "event", ID: string(it.SourceEvent), Note: "earlier"}, {Kind: "event", ID: string(ev.ID), Note: "later"}}})
}
}
return out, nil
}
func toSet(ws []string) map[string]struct{} {
m := make(map[string]struct{}, len(ws))
for _, w := range ws {
m[w] = struct{}{}
}
return m
}