fix(cognition): decisions and cycle output no longer depend on map order
Formation summed its terms by ranging over a map, so the score could differ in the last bit between runs of the same state; the volition hook assessed commitments, metacognition raised repeated-failure candidates and the forensic explanation listed commitments in map iteration order, so the events a cycle emits and an explanation's order varied between runs. Each now iterates in sorted order, as replay determinism (PLAN §1 R3) requires.
This commit is contained in:
@@ -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)
|
x.Resolution = append(x.Resolution, d.Kind+" ("+d.Outcome+"): "+d.Rationale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cids := make([]ids.ID, 0, len(commitmentIDs))
|
||||||
for id := range 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 {
|
if c, err := s.Volition.GetCommitment(ctx, nil, mind, id); err == nil {
|
||||||
x.Commitments = append(x.Commitments, c)
|
x.Commitments = append(x.Commitments, c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -93,8 +94,13 @@ func (Metacognition) Observe(ctx context.Context, s *brain.State) ([]brain.Candi
|
|||||||
bySubject[b.Subject]++
|
bySubject[b.Subject]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for subj, n := range bySubject {
|
subjects := make([]string, 0, len(bySubject))
|
||||||
if n >= 3 {
|
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})
|
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]++
|
byAction[capName]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for capName, n := range byAction {
|
names := make([]string, 0, len(byAction))
|
||||||
if n >= 3 {
|
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})
|
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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
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) {
|
switch volition.Assess(cm, c.State.Now) {
|
||||||
case volition.StateDue:
|
case volition.StateDue:
|
||||||
if err := vs.MarkDue(ctx, r, cm); err != nil {
|
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) {
|
for _, cf := range volition.CommitmentConflicts(live, c.State.Now) {
|
||||||
key := pairKey(cf.A.ID, cf.B.ID)
|
key := pairKey(cf.A.ID, cf.B.ID)
|
||||||
if recentlyRaised(ctx, c, events.CommitmentConflict, "pair", key) {
|
if recentlyRaised(ctx, c, events.CommitmentConflict, "pair", key) {
|
||||||
|
|||||||
@@ -688,9 +688,16 @@ func Form(in FormationInput) Formation {
|
|||||||
case w.CommitmentID != "":
|
case w.CommitmentID != "":
|
||||||
f.Kind = IntentFulfilCommitment
|
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
|
score := 0.0
|
||||||
for _, v := range t {
|
for _, k := range keys {
|
||||||
score += v
|
score += t[k]
|
||||||
}
|
}
|
||||||
f.Score = round3(score)
|
f.Score = round3(score)
|
||||||
t["score"] = f.Score
|
t["score"] = f.Score
|
||||||
|
|||||||
Reference in New Issue
Block a user