Archived
give: take a module's own secret through a hidden prompt at the operator's desk (hq ADR 0259 §10) #156
@@ -116,8 +116,11 @@ type asker struct {
|
||||
routerHere func(ctx context.Context) (bool, error)
|
||||
// channels is what the channels are now, as a fingerprint: who holds which kind, promising what.
|
||||
channels func(ctx context.Context) string
|
||||
now func() time.Time
|
||||
logf func(string, ...any)
|
||||
// raise keeps the asker's own condition (sourceAsker): which conditions needing the operator could not be
|
||||
// asked, and why. Nil raises nothing (a test that does not look).
|
||||
raise func(ctx context.Context, obs []conditions.Observation) error
|
||||
now func() time.Time
|
||||
logf func(string, ...any)
|
||||
|
||||
saidNoRouter bool
|
||||
|
||||
@@ -187,7 +190,18 @@ func (a *asker) reconcile(ctx context.Context) error {
|
||||
"named by its caller, assigned): the operator is asked nothing until one is", broker.AsksSeat)
|
||||
a.saidNoRouter = true
|
||||
}
|
||||
return nil
|
||||
open, err := a.open(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var unasked []conditions.Condition
|
||||
for _, c := range open {
|
||||
if wants(c, now) {
|
||||
unasked = append(unasked, c)
|
||||
}
|
||||
}
|
||||
return a.sayUnasked(ctx, unasked, "no router takes the controller's asks: no module holding "+
|
||||
broker.AsksSeat+" that takes an ask under its asker's name is assigned")
|
||||
}
|
||||
a.saidNoRouter = false
|
||||
}
|
||||
@@ -248,6 +262,8 @@ func (a *asker) reconcile(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
wanted := map[string]bool{}
|
||||
var unasked []conditions.Condition // refused by the router, and nothing it was refused for changed
|
||||
var refusedWords []string
|
||||
// The most urgent first, then the oldest: those are asked when no more than askMostOpen may be.
|
||||
sort.SliceStable(open, func(i, j int) bool {
|
||||
ui, uj := open[i].Severity == conditions.Urgent, open[j].Severity == conditions.Urgent
|
||||
@@ -272,6 +288,10 @@ func (a *asker) reconcile(ctx context.Context) error {
|
||||
wanted[c.Key] = true
|
||||
if r, was := refused[c.Key]; was && sameAsked(r.Actions, c.Actions) && r.Channels == channels {
|
||||
if _, held := byCondition[c.Key]; !held {
|
||||
unasked = append(unasked, c)
|
||||
if r.Warrant != nil && r.Warrant.Words != "" {
|
||||
refusedWords = append(refusedWords, r.Warrant.Words)
|
||||
}
|
||||
continue // refused, and nothing it was refused for has changed
|
||||
}
|
||||
}
|
||||
@@ -313,6 +333,45 @@ func (a *asker) reconcile(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
why := "the router refused the ask"
|
||||
if len(refusedWords) > 0 {
|
||||
why += ": " + refusedWords[0]
|
||||
}
|
||||
return a.sayUnasked(ctx, unasked, why)
|
||||
}
|
||||
|
||||
// sourceAsker raises the asker's own condition.
|
||||
const sourceAsker = "asker"
|
||||
|
||||
// sayUnasked keeps the asker's one condition: while a condition that needs the operator could not be asked
|
||||
// on any channel, said loudly (failure must be loud), cleared when every one could be.
|
||||
func (a *asker) sayUnasked(ctx context.Context, unasked []conditions.Condition, why string) error {
|
||||
if a.raise == nil {
|
||||
return nil
|
||||
}
|
||||
var obs []conditions.Observation
|
||||
if len(unasked) > 0 {
|
||||
keys := make([]string, 0, len(unasked))
|
||||
severity := conditions.Warning
|
||||
for _, c := range unasked {
|
||||
keys = append(keys, c.Key)
|
||||
if c.Severity == conditions.Urgent {
|
||||
severity = conditions.Urgent
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
obs = append(obs, conditions.Observation{Scope: conditions.ScopeSeat, ID: broker.AsksSeat, Token: "unasked",
|
||||
Kind: "asks-undelivered", Severity: severity, Source: sourceAsker,
|
||||
Summary: fmt.Sprintf("%d condition(s) that need the operator could not be asked on any channel: %s; %s",
|
||||
len(keys), strings.Join(keys, ", "), why),
|
||||
Headline: "Questions for you not delivered",
|
||||
Explanation: "Needs you: answer them from the mesh MCP server. The mesh could not send you its questions on any channel.",
|
||||
Needs: "answer them from the mesh MCP server, and check why no channel carries them.",
|
||||
Resolved: "The mesh can ask you again"})
|
||||
}
|
||||
if err := a.raise(ctx, obs); err != nil {
|
||||
a.logf("whether the operator could be asked could not be kept as a condition: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -514,3 +514,52 @@ func TestAWarrantPerformsOnlyTheActItsOptionBound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Failure is loud (novox/hq ADR 0259, the self-review of 2026-10-09): a condition that needs the operator and
|
||||
// could not be asked on any channel — no router, or the router refused the ask — is a condition of its own,
|
||||
// cleared once it can be asked again.
|
||||
func TestAnAskThatCannotBeDeliveredIsSaid(t *testing.T) {
|
||||
r := newAskerRig(t)
|
||||
var raised [][]conditions.Observation
|
||||
r.a.raise = func(_ context.Context, obs []conditions.Observation) error {
|
||||
raised = append(raised, obs)
|
||||
return nil
|
||||
}
|
||||
last := func() []conditions.Observation { return raised[len(raised)-1] }
|
||||
routerHere := false
|
||||
r.a.routerHere = func(context.Context) (bool, error) { return routerHere, nil }
|
||||
channels := "channel/telegram=telegram@anchor[choice]own:true"
|
||||
r.a.channels = func(context.Context) string { return channels }
|
||||
r.open = []conditions.Condition{heldCondition()}
|
||||
|
||||
_ = r.a.reconcile(context.Background())
|
||||
if got := last(); len(got) != 1 || got[0].Kind != "asks-undelivered" ||
|
||||
!strings.Contains(got[0].Summary, heldCondition().Key) || !strings.Contains(got[0].Summary, "no router") {
|
||||
t.Fatalf("no router, said as %+v", got)
|
||||
}
|
||||
|
||||
routerHere = true
|
||||
_ = r.a.reconcile(context.Background())
|
||||
if got := last(); len(got) != 0 {
|
||||
t.Fatalf("asked, and still said undelivered: %+v", got)
|
||||
}
|
||||
first := r.asksSent(t)[0]
|
||||
refusal, _ := json.Marshal(asks.Warrant{Ask: first.ID, Asker: "mesh-controller", Outcome: asks.OutcomeRefused,
|
||||
Words: "no channel can carry any of its answers now", At: r.now})
|
||||
if err := r.a.Decided(context.Background(), refusal); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = r.a.reconcile(context.Background())
|
||||
if got := last(); len(got) != 1 || !strings.Contains(got[0].Summary, "no channel can carry") {
|
||||
t.Fatalf("the router's refusal, said as %+v", got)
|
||||
}
|
||||
if why, ok := conditions.PlainWords(conditions.Words{Headline: last()[0].Headline, Explanation: last()[0].Explanation,
|
||||
Needs: last()[0].Needs, Resolved: last()[0].Resolved}, ""); !ok {
|
||||
t.Errorf("not plain: %s", why)
|
||||
}
|
||||
r.open = nil
|
||||
_ = r.a.reconcile(context.Background())
|
||||
if got := last(); len(got) != 0 {
|
||||
t.Errorf("nothing needs asking, and still said: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,8 +283,11 @@ func startAsking(ctx context.Context, open *stores, server *link.Server, conn *n
|
||||
routerRecord: routerRecordOf(conn, open.inventory),
|
||||
routerHere: routerHereIn(open.inventory),
|
||||
channels: channelsIn(open.inventory),
|
||||
now: time.Now,
|
||||
logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
|
||||
raise: func(ctx context.Context, obs []conditions.Observation) error {
|
||||
return keeper.Reconcile(ctx, sourceAsker, obs)
|
||||
},
|
||||
now: time.Now,
|
||||
logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
|
||||
}
|
||||
if err := server.Decides(a); err != nil {
|
||||
fmt.Printf("the operator's answers cannot be heard, so nothing is asked: %v\n", err)
|
||||
|
||||
Reference in New Issue
Block a user