diff --git a/internal/agency/agency.go b/internal/agency/agency.go index 92950d9..bd988c8 100644 --- a/internal/agency/agency.go +++ b/internal/agency/agency.go @@ -381,7 +381,12 @@ func (s *Service) Explain(ctx context.Context, mind ids.ID, ref Ref) (Explanatio x.Resolution = append(x.Resolution, d.Kind+" ("+d.Outcome+"): "+d.Rationale) } } + cids := make([]ids.ID, 0, len(commitmentIDs)) for id := range commitmentIDs { + cids = append(cids, id) + } + sort.Slice(cids, func(i, j int) bool { return cids[i] < cids[j] }) + for _, id := range cids { if c, err := s.Volition.GetCommitment(ctx, nil, mind, id); err == nil { x.Commitments = append(x.Commitments, c) } diff --git a/internal/cognition/metacognition.go b/internal/cognition/metacognition.go index b3fe7fb..67174fc 100644 --- a/internal/cognition/metacognition.go +++ b/internal/cognition/metacognition.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "strings" "time" @@ -93,8 +94,13 @@ func (Metacognition) Observe(ctx context.Context, s *brain.State) ([]brain.Candi bySubject[b.Subject]++ } } - for subj, n := range bySubject { - if n >= 3 { + subjects := make([]string, 0, len(bySubject)) + for subj := range bySubject { + subjects = append(subjects, subj) + } + sort.Strings(subjects) + for _, subj := range subjects { + if n := bySubject[subj]; n >= 3 { add("repeated_failure", subj, fmt.Sprintf("%d hypotheses about %q were rejected this month", n, subj), 0.6, nil, map[string]any{"subject": subj, "count": n}) } } @@ -260,8 +266,13 @@ func volitionChecks(ctx context.Context, s *brain.State, add func(check, ref, su byAction[capName]++ } } - for capName, n := range byAction { - if n >= 3 { + names := make([]string, 0, len(byAction)) + for capName := range byAction { + names = append(names, capName) + } + sort.Strings(names) // candidates in a stable order, for replay + for _, capName := range names { + if n := byAction[capName]; n >= 3 { add("repeated_execution_failure", capName, fmt.Sprintf("%s failed %d times in plans today", capName, n), 0.6, nil, map[string]any{"capability": capName, "count": n}) } } diff --git a/internal/cognition/volition.go b/internal/cognition/volition.go index bfed809..64d277d 100644 --- a/internal/cognition/volition.go +++ b/internal/cognition/volition.go @@ -137,7 +137,14 @@ func (VolitionHook) AfterCycle(ctx context.Context, c *brain.Cycle, emitted []ev } } } + // Commitments in id order, so the events a cycle emits do not depend on + // map iteration order. + live := make([]volition.Commitment, 0, len(dctx.Commitments)) for _, cm := range dctx.Commitments { + live = append(live, cm) + } + sort.Slice(live, func(i, j int) bool { return live[i].ID < live[j].ID }) + for _, cm := range live { switch volition.Assess(cm, c.State.Now) { case volition.StateDue: if err := vs.MarkDue(ctx, r, cm); err != nil { @@ -159,10 +166,6 @@ func (VolitionHook) AfterCycle(ctx context.Context, c *brain.Cycle, emitted []ev } } } - var live []volition.Commitment - for _, cm := range dctx.Commitments { - live = append(live, cm) - } for _, cf := range volition.CommitmentConflicts(live, c.State.Now) { key := pairKey(cf.A.ID, cf.B.ID) if recentlyRaised(ctx, c, events.CommitmentConflict, "pair", key) { diff --git a/internal/volition/rules.go b/internal/volition/rules.go index 4552c28..dcd021a 100644 --- a/internal/volition/rules.go +++ b/internal/volition/rules.go @@ -688,9 +688,16 @@ func Form(in FormationInput) Formation { case w.CommitmentID != "": f.Kind = IntentFulfilCommitment } + // Summed in key order: floating-point addition is not associative, and a + // decision must not depend on map iteration order. + keys := make([]string, 0, len(t)) + for k := range t { + keys = append(keys, k) + } + sort.Strings(keys) score := 0.0 - for _, v := range t { - score += v + for _, k := range keys { + score += t[k] } f.Score = round3(score) t["score"] = f.Score