Tell the operator when the answer to a plan's ask is refused (issue 1316, ADR 0449)

This commit is contained in:
2026-10-19 21:30:33 +00:00
parent 7770d193da
commit 12db1d3748
3 changed files with 138 additions and 2 deletions
+35 -2
View File
@@ -208,8 +208,11 @@ type asker struct {
// 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)
// tell tells the operator one plain notice that needs no answer (operator-channel.tell, novox/hq ADR 0380):
// the refusal of an answer to a plan's ask (ADR 0449 §3). Nil tells nothing (a test that does not look).
tell func(ctx context.Context, headline string, lines []string) error
now func() time.Time
logf func(string, ...any)
saidNoRouter bool
saidNoGrant bool
@@ -886,6 +889,7 @@ func (a *asker) Decided(ctx context.Context, body []byte) error {
ended := a.now()
if acted != nil {
outcome = "failed: " + acted.Error()
a.tellRefused(ctx, *r, act, acted)
}
r.Ended, r.Acted = ended, outcome
if _, err := a.store.Change(ctx, r.ID, func(x *asked) bool {
@@ -920,6 +924,35 @@ func (a *asker) Decided(ctx context.Context, body []byte) error {
return nil
}
// tellRefused tells the operator that the answer they gave to a plan's ask was refused (novox/hq ADR 0449 §3): the
// controller's `plans` verb refuses a Retry, Start or Stop when the plan moved on between the ask and the answer,
// and the refusal was only recorded, so the operator who pressed the answer heard nothing. One plain notice with the
// ask's headline and the refusal's words, beside the hand act the caller still records. An answer to any other ask
// is not told here. A notice that cannot be told is said in the log and changes nothing.
func (a *asker) tellRefused(ctx context.Context, r asked, act conditions.Action, refusal error) {
if a.tell == nil || act.Verb != broker.ControllerSeat+".plans" {
return
}
lines := []string{
fmt.Sprintf("Your answer, %s, could not be done: %s.", act.Label, strings.TrimRight(refusalWords(refusal), ".")),
"Nothing was changed.",
}
if err := a.tell(ctx, r.Ask.Headline, lines); err != nil {
a.logf("the operator could NOT be told that %s was refused (ask %s): %v", act.Label, r.ID, err)
}
}
// refusalWords is a refused verb's words as the operator reads them: without the verb's address in front, with the
// plans named as "the plan" (plainRefusal), and, where they still carry an identifier the operator's channel
// withholds, only that the controller refused it.
func refusalWords(err error) string {
text := err.Error()
if i := strings.Index(text, " refused: "); i >= 0 {
text = text[i+len(" refused: "):]
}
return plainRefusal(text)
}
// handActWarrant is the verb an act the operator chose on a warrant is recorded under: a person's decision,
// never a repair (handActVerbs).
const handActWarrant = "warrant"