feat: add immutable cognitive event store, live streams and event explorer

Phase 3. Events are the append-only record of everything a mind does
(ADR-0002). pkg/events defines the closed type vocabulary, categories,
evidence references and the observation payload with explicit trust
levels. The store validates and appends inside the caller's transaction,
publishes to a bounded fan-out bus only after commit, queries by seq
cursor with type/category/source/salience/confidence/time/search
filters, and walks provenance: cause, parents, children, cited evidence,
derived events and correlation groups. Durable per-consumer cursors never
move backwards.

Server-Sent Events streams (per mind and global) subscribe before
replaying history after Last-Event-ID so nothing is missed, apply the
same filter vocabulary live, and heartbeat. Operators can inject
observations tagged with operator trust for demos and tests.

UI: live thought stream with filters and paging, forensic event page
with causal chain and evidence navigation, dashboard important events.
Personality and status changes now leave operator_action events in the
mind's own history. CLI: event inspect/list/tail, observe.
This commit is contained in:
Andy Dixon
2026-06-17 20:02:22 +01:00
parent fb34f08f40
commit 85d769b245
29 changed files with 1866 additions and 25 deletions
+8 -1
View File
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/andydixon/noema/internal/auth"
evstore "github.com/andydixon/noema/internal/events"
"github.com/andydixon/noema/internal/storage"
"github.com/andydixon/noema/pkg/ids"
)
@@ -97,7 +98,13 @@ func (u *UI) setupSubmit(w http.ResponseWriter, r *http.Request) {
func (u *UI) dashboard(w http.ResponseWriter, r *http.Request) {
entries, _ := u.deps.Audit.List(r.Context(), 0, 8, "", "")
u.render(w, r, "dashboard", "Dashboard", map[string]any{"Minds": 0, "Status": u.statusF(r), "Audit": entries})
mindsList, _ := u.deps.Minds.List(r.Context(), false)
names := map[ids.ID]string{}
for _, m := range mindsList {
names[m.ID] = m.DisplayName
}
evs, _ := u.deps.Events.Query(r.Context(), evstore.Filter{MinSalience: 0.6, Limit: 12})
u.render(w, r, "dashboard", "Dashboard", map[string]any{"Minds": mindsList, "MindNames": names, "Events": evs, "Status": u.statusF(r), "Audit": entries})
}
func (u *UI) system(w http.ResponseWriter, r *http.Request) {