5ac7347c38
The two claimant sets that live in the daemon are where the arbitration is least visible: both are a hardcoded order of functions that each answer 'is this mine?' alone. The ladder declares its roster up front, so a rung that never ran is named rather than omitted, and the query chain does the same for the sources below the one that claimed. Recording is installed in runTurn and not in the IPC entry point, so the mic, telegram and the web leave the same trail. A record only the web produced would be missing exactly the turns that are hardest to reproduce.
133 lines
5.0 KiB
Go
133 lines
5.0 KiB
Go
// mavend/decisiontrace.go — the daemon's half of the per-turn decision record.
|
|
//
|
|
// V-564. The router says what the cascade did (internal/router/decisiontrace.go);
|
|
// this file covers the two claimant sets that live in the daemon: the stateful
|
|
// resolvers that run BEFORE routing and pre-empt it unconditionally, and the
|
|
// query source chain that runs after. Those two are where the arbitration is
|
|
// least visible, because both are a hardcoded order of functions that each
|
|
// answer "is this mine?" alone and none of which answers "is this more mine
|
|
// than yours?" (V-558).
|
|
//
|
|
// Recording changes no route. Every helper here is a no-op on a context with no
|
|
// record, which is what every test that does not ask for one gets.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/kami/maven/internal/decision"
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// preRouteLadder — the resolvers runTurn offers the utterance to before the
|
|
// router sees it, in the order they get their say. Kept here as a roster rather
|
|
// than derived from the code, so a resolver that returns early and skips the
|
|
// rest still leaves the rest NAMED in the record: a claimant that never looked
|
|
// and one that looked and passed are the distinction the ordering hides, and
|
|
// they are the difference between a bug in the ladder and a bug in a resolver.
|
|
//
|
|
// Adding a step to runTurn means adding its name here. Nothing enforces that,
|
|
// and nothing should: a missing name costs one line of the record, while a
|
|
// check that walks the ladder would have to run the ladder.
|
|
var preRouteLadder = []string{
|
|
"confirm", "clarify-answer", "quiet-toggle", "snooze", "ack", "repair", "ordinal",
|
|
}
|
|
|
|
// notePreRoute records one rung of that ladder and passes its verdict through
|
|
// unchanged, so the call site stays the single `if handled` it already was.
|
|
func notePreRoute(ctx context.Context, name string, handled bool) bool {
|
|
rec := decision.From(ctx)
|
|
if rec == nil {
|
|
return handled
|
|
}
|
|
if handled {
|
|
rec.Note(decision.Claim{
|
|
Stage: decision.StagePreRoute, Claimant: name, Outcome: decision.Won,
|
|
Reason: "it pre-empted routing, so the router never saw this turn",
|
|
})
|
|
return handled
|
|
}
|
|
rec.Note(decision.Claim{
|
|
Stage: decision.StagePreRoute, Claimant: name, Outcome: decision.Declined,
|
|
Reason: "nothing of its own was pending",
|
|
})
|
|
return handled
|
|
}
|
|
|
|
// noteTerminal records whoever actually produced the reply, but only if the
|
|
// turn is still unclaimed. A route decides the intent; it does not answer, and
|
|
// on a thinned route or a plain act nothing downstream keeps a scoreboard. So
|
|
// the record would otherwise close with an empty winner, which reads as a lost
|
|
// turn instead of an asked question.
|
|
func noteTerminal(ctx context.Context, claimant string, intent router.Intent, reason string) {
|
|
decision.From(ctx).NoteIfUnclaimed(decision.Claim{
|
|
Stage: decision.StageAction, Claimant: claimant,
|
|
Intent: string(intent), Reason: reason,
|
|
})
|
|
}
|
|
|
|
// noteMerge records the follow-up merge, which is the one claimant that edits
|
|
// the winning decision instead of taking the turn from it. It is compared on
|
|
// the four slots the merge can fill, because a Decision holds a slice and is
|
|
// not comparable.
|
|
func noteMerge(ctx context.Context, before, after router.Decision) {
|
|
rec := decision.From(ctx)
|
|
if rec == nil {
|
|
return
|
|
}
|
|
changed := before.Slots.HasTime != after.Slots.HasTime ||
|
|
before.Slots.HasKey != after.Slots.HasKey ||
|
|
before.Slots.HasFn != after.Slots.HasFn ||
|
|
before.Slots.Text != after.Slots.Text ||
|
|
before.Intent != after.Intent
|
|
if !changed {
|
|
rec.Note(decision.Claim{
|
|
Stage: decision.StageMerge, Claimant: "follow-up-merge", Outcome: decision.Declined,
|
|
Reason: "no slot of this turn was left for a previous one to fill",
|
|
})
|
|
return
|
|
}
|
|
rec.Note(decision.Claim{
|
|
Stage: decision.StageMerge, Claimant: "follow-up-merge", Intent: string(after.Intent),
|
|
Outcome: decision.Merged, Reason: "filled this turn's gaps from the previous turn",
|
|
})
|
|
}
|
|
|
|
// turnDecisionsFn — the reader mavweb gets, or nil when voice was never wired.
|
|
// Same shape as intakeEventsFn: the daemon holds the ring, the IPC layer only
|
|
// converts it.
|
|
func turnDecisionsFn(w *voiceWiring) func(int) []ipc.TurnDecision {
|
|
if w == nil || w.handler == nil || w.handler.decisions == nil {
|
|
return nil
|
|
}
|
|
ring := w.handler.decisions
|
|
return func(n int) []ipc.TurnDecision {
|
|
recs := ring.Recent(n)
|
|
out := make([]ipc.TurnDecision, 0, len(recs))
|
|
for _, rec := range recs {
|
|
claims := make([]ipc.TurnClaim, 0, len(rec.Claims))
|
|
for _, c := range rec.Claims {
|
|
claims = append(claims, ipc.TurnClaim{
|
|
Stage: c.Stage, Claimant: c.Claimant, Intent: c.Intent,
|
|
Score: c.Score, HasScore: c.HasScore,
|
|
Outcome: c.Outcome, Reason: c.Reason,
|
|
})
|
|
}
|
|
out = append(out, ipc.TurnDecision{
|
|
Ts: rec.Ts, Utterance: rec.Utterance, Winner: rec.Winner, Claims: claims,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
}
|
|
|
|
// querySourceNames — the query chain's roster, in chain order.
|
|
func querySourceNames() []string {
|
|
names := make([]string, len(querySources))
|
|
for i, src := range querySources {
|
|
names[i] = src.name
|
|
}
|
|
return names
|
|
}
|