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
+18
View File
@@ -5,6 +5,8 @@
package config
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -244,3 +246,19 @@ func (c Config) Redacted() Config {
}
return r
}
// Digest is a stable hash of the non-secret configuration: it changes when
// the daemon would behave differently and stays the same when only secrets
// or credentials rotate. Used to explain replay differences.
func (c Config) Digest() string {
public := struct {
HTTPAddr, Env, LogLevel, LogFormat, ResearchDir string
MaxWorkers, MaxConcurrentLLM int
DailyTokenBudget, MaxRequestBody int64
SessionIdleTTL, SessionAbsoluteTTL time.Duration
HasMasterKey bool
}{c.HTTPAddr, c.Env, c.LogLevel, c.LogFormat, c.ResearchDir, c.MaxWorkers, c.MaxConcurrentLLM, c.DailyTokenBudget, c.MaxRequestBody, c.SessionIdleTTL, c.SessionAbsoluteTTL, c.MasterKey != ""}
b, _ := json.Marshal(public)
sum := sha256.Sum256(b)
return "sha256:" + hex.EncodeToString(sum[:8])
}