feat(replay): record runtime identity; label compatibility; analyse first divergence

Snapshots, replays and experiments now record the runtime they ran under:
build (module version, VCS commit, modified tree, commit time, Go version,
platform), schema version, a digest of the non-secret configuration that
ignores secret and credential changes, personality version, policy digest,
model routing including per-mind overrides, prompt versions, deterministic
subsystem versions and timezone. A replay compares its identity with the
snapshot's and is an exact-candidate, a different-build (differences
listed) or incompatible (schema differs; the replay stops before running).
The first non-exact step is analysed into events only in the recording,
events only in the replay, and the components they belong to, labelled as
inferred; live model calls are named as their own source of difference.
Migration 0022 is additive; unrecorded identities are reported as such.
This commit is contained in:
Andy Dixon
2026-08-20 21:11:27 +01:00
parent 37b9f0e2f5
commit 522de3b026
18 changed files with 648 additions and 51 deletions
+24 -19
View File
@@ -25,6 +25,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
evstore "github.com/andydixon/noema/internal/events"
"github.com/andydixon/noema/internal/identity"
"github.com/andydixon/noema/internal/minds"
"github.com/andydixon/noema/internal/storage"
"github.com/andydixon/noema/pkg/ids"
@@ -335,23 +336,25 @@ func remapValue(v any, remap map[string]string) any {
// Snapshot is a stored archive.
type Snapshot struct {
ID ids.ID `json:"id"`
MindID ids.ID `json:"mind_id"`
Name string `json:"name"`
Note string `json:"note"`
EventSeq int64 `json:"event_seq"`
Checksum string `json:"checksum"`
SizeBytes int `json:"size_bytes"`
Counts map[string]int `json:"counts"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
ID ids.ID `json:"id"`
MindID ids.ID `json:"mind_id"`
Name string `json:"name"`
Note string `json:"note"`
EventSeq int64 `json:"event_seq"`
Checksum string `json:"checksum"`
SizeBytes int `json:"size_bytes"`
Counts map[string]int `json:"counts"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
Identity identity.Identity `json:"identity"` // the runtime that captured it
}
// Store persists snapshots and performs forks and imports.
type Store struct {
DB *pgxpool.Pool
Events *evstore.Store
Minds *minds.Store
DB *pgxpool.Pool
Events *evstore.Store
Minds *minds.Store
Identity *identity.Capturer // nil records build information only
}
// Create captures and stores a snapshot.
@@ -365,21 +368,23 @@ func (s *Store) Create(ctx context.Context, m minds.Mind, name, note, by string)
}
raw, _ := json.Marshal(a)
counts, _ := json.Marshal(a.Counts())
sn := Snapshot{ID: ids.New(), MindID: m.ID, Name: name, Note: note, EventSeq: a.Source.EventSeq, Checksum: a.Checksum, SizeBytes: len(raw), Counts: a.Counts(), CreatedBy: by, CreatedAt: time.Now()}
_, err = s.DB.Exec(ctx, `INSERT INTO snapshots (id, mind_id, name, note, event_seq, checksum, size_bytes, counts, archive, created_by, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`,
sn.ID, sn.MindID, sn.Name, sn.Note, sn.EventSeq, sn.Checksum, sn.SizeBytes, counts, raw, sn.CreatedBy, sn.CreatedAt)
sn := Snapshot{ID: ids.New(), MindID: m.ID, Name: name, Note: note, EventSeq: a.Source.EventSeq, Checksum: a.Checksum, SizeBytes: len(raw), Counts: a.Counts(), CreatedBy: by, CreatedAt: time.Now(), Identity: s.Identity.Capture(ctx, m.ID)}
idj, _ := json.Marshal(sn.Identity)
_, err = s.DB.Exec(ctx, `INSERT INTO snapshots (id, mind_id, name, note, event_seq, checksum, size_bytes, counts, archive, created_by, created_at, identity) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)`,
sn.ID, sn.MindID, sn.Name, sn.Note, sn.EventSeq, sn.Checksum, sn.SizeBytes, counts, raw, sn.CreatedBy, sn.CreatedAt, idj)
return sn, err
}
const snapCols = `id, mind_id, name, note, event_seq, checksum, size_bytes, counts, created_by, created_at`
const snapCols = `id, mind_id, name, note, event_seq, checksum, size_bytes, counts, created_by, created_at, identity`
func scanSnap(row pgx.Row) (Snapshot, error) {
var sn Snapshot
var counts []byte
if err := row.Scan(&sn.ID, &sn.MindID, &sn.Name, &sn.Note, &sn.EventSeq, &sn.Checksum, &sn.SizeBytes, &counts, &sn.CreatedBy, &sn.CreatedAt); err != nil {
var counts, idj []byte
if err := row.Scan(&sn.ID, &sn.MindID, &sn.Name, &sn.Note, &sn.EventSeq, &sn.Checksum, &sn.SizeBytes, &counts, &sn.CreatedBy, &sn.CreatedAt, &idj); err != nil {
return sn, storage.Translate(err)
}
json.Unmarshal(counts, &sn.Counts) //nolint:errcheck
json.Unmarshal(idj, &sn.Identity) //nolint:errcheck
return sn, nil
}