fix(volition): rest an action on the belief about that action

An intention's supporting beliefs came from a text search on the target,
and ties on rank and confidence had no order, so a verification miss could
revise an unrelated belief about the same service. Beliefs that mention the
proposed action now come first, and belief search breaks ties by id.

The incident scenario asserts the first support is the belief about
restarting, and waits for the blocked intention and the procedural memory
instead of reading them once.
This commit is contained in:
Andy Dixon
2026-10-04 19:46:55 +01:00
parent f609fae746
commit edcc6580b6
3 changed files with 24 additions and 10 deletions
+1 -1
View File
@@ -872,7 +872,7 @@ func (s *Store) Search(ctx context.Context, mind ids.ID, text string, limit int)
} }
q := strings.Join(toks, " | ") q := strings.Join(toks, " | ")
rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status NOT IN ('superseded') AND b.search @@ to_tsquery('english', $2) rows, err := s.db.Query(ctx, `SELECT `+cols+` FROM beliefs b WHERE b.mind_id=$1 AND b.status NOT IN ('superseded') AND b.search @@ to_tsquery('english', $2)
ORDER BY ts_rank_cd(b.search, to_tsquery('english', $2)) DESC, b.confidence DESC LIMIT $3`, mind, q, limit) ORDER BY ts_rank_cd(b.search, to_tsquery('english', $2)) DESC, b.confidence DESC, b.id LIMIT $3`, mind, q, limit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+8
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sort"
"strings" "strings"
"time" "time"
@@ -495,6 +496,13 @@ func supportingBeliefs(ctx context.Context, s *brain.State, w volition.Want) []i
if err != nil { if err != nil {
return nil return nil
} }
// A belief about the proposed action itself ("restarting atlas-worker
// clears the queue") is what the action rests on, so it comes first; the
// first supporting belief is the one a verification outcome revises.
action := strings.ToLower(w.Proposal.Capability[strings.LastIndex(w.Proposal.Capability, ".")+1:])
sort.SliceStable(bs, func(i, j int) bool {
return action != "" && strings.Contains(strings.ToLower(bs[i].Proposition), action) && !strings.Contains(strings.ToLower(bs[j].Proposition), action)
})
var out []ids.ID var out []ids.ID
for _, b := range bs { for _, b := range bs {
if len(out) == 3 { if len(out) == 3 {
+15 -9
View File
@@ -72,7 +72,8 @@ func TestIncidentScenario(t *testing.T) {
var req string var req string
waitFor(t, h, m, 30*time.Second, "a capability request for logs", func() bool { waitFor(t, h, m, 30*time.Second, "a capability request for logs", func() bool {
rs, _ := h.Deps.Capabilities.CapabilityRequests(ctx, m.ID, "pending", 5) rs, _ := h.Deps.Capabilities.CapabilityRequests(ctx, m.ID, "pending", 5)
if len(rs) == 1 { // The request commits before the executor blocks the intention.
if cur, _ := h.Deps.Volition.GetIntention(ctx, nil, m.ID, investigate.ID); len(rs) == 1 && cur.Status == volition.IntentionBlocked {
req = rs[0].ID.String() req = rs[0].ID.String()
if rs[0].Capability != "logs.query" || rs[0].Risk == "" || rs[0].Fallback == "" || rs[0].MaxOperations == nil { if rs[0].Capability != "logs.query" || rs[0].Risk == "" || rs[0].Fallback == "" || rs[0].MaxOperations == nil {
t.Fatalf("request not explained: %+v", rs[0]) t.Fatalf("request not explained: %+v", rs[0])
@@ -139,6 +140,9 @@ func TestIncidentScenario(t *testing.T) {
if host.count() != 0 || restart.Kind != volition.IntentRequestApproval || len(restart.BeliefIDs) == 0 { if host.count() != 0 || restart.Kind != volition.IntentRequestApproval || len(restart.BeliefIDs) == 0 {
t.Fatalf("restart before approval, wrong kind or no supporting belief: restarts=%d %+v", host.count(), restart) t.Fatalf("restart before approval, wrong kind or no supporting belief: restarts=%d %+v", host.count(), restart)
} }
if restart.BeliefIDs[0] != belief.ID {
t.Fatalf("the belief about restarting should be the first support, got %v (want %s)", restart.BeliefIDs, belief.ID)
}
// 21–25: approved; restarted with a typed capability; health is verified, not assumed; it fails. // 21–25: approved; restarted with a typed capability; health is verified, not assumed; it fails.
post("/api/v1/approvals/"+approval+"/approve", map[string]any{"note": "go"}) post("/api/v1/approvals/"+approval+"/approve", map[string]any{"note": "go"})
@@ -182,14 +186,16 @@ func TestIncidentScenario(t *testing.T) {
} }
return false return false
}) })
mems, _ := h.Deps.Memory.List(ctx, m.ID, memory.Filter{Kinds: []concepts.MemoryKind{concepts.Procedural}, Limit: 10}) // The plan completes, and records what worked, after the prediction resolves.
learnt := false waitFor(t, h, m, 30*time.Second, "what worked recorded as a procedure", func() bool {
for _, mm := range mems { mems, _ := h.Deps.Memory.List(ctx, m.ID, memory.Filter{Kinds: []concepts.MemoryKind{concepts.Procedural}, Limit: 10})
learnt = learnt || strings.Contains(mm.Content, "confirmed by observation") for _, mm := range mems {
} if strings.Contains(mm.Content, "confirmed by observation") {
if !learnt { return true
t.Fatalf("what worked was not recorded as a procedure: %+v", mems) }
} }
return false
})
// 31: "Why did you do that?" is answered from the recorded chain. // 31: "Why did you do that?" is answered from the recorded chain.
execs, _ := h.Deps.Capabilities.Executions(ctx, m.ID, 50) execs, _ := h.Deps.Capabilities.Executions(ctx, m.ID, 50)