package console // mesh_wait: a session waits on the bus for one event instead of polling (novox/hq ADR 0367). // // **Only the families the tool runner was issued** (§2, §6): its membership names them, composed by the // controller from the tool runner's manifest together with the grant that lets this account subscribe them. // A name not listed there is refused here, before the bus would refuse it; no argument or setting adds one. // **An event only wakes the session** (§4): what it says is read again from the verb that owns it. import ( "bytes" "context" "encoding/json" "fmt" "math" "sort" "strings" "time" ) // WaitDefault is how long a wait lasts when no timeout is given, and WaitLongest the most it may last // (ADR 0367 §1): four minutes and ten. var ( WaitDefault = 4 * time.Minute WaitLongest = 10 * time.Minute ) func waitTool() map[string]any { return map[string]any{"name": verbWait, "inputSchema": map[string]any{"type": "object", "properties": map[string]any{ "subjects": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "one or more event families to wait on, by name, e.g. `mesh-knowledge.reviewed`; only " + "those the mesh granted this machine's tool runner — a refusal names them"}, "filter": map[string]any{"type": "object", "description": "fields of the event's body that must equal the given values, e.g. {\"kind\": \"decision\", \"number\": 367} (optional)"}, "timeout": map[string]any{"type": "number", "description": fmt.Sprintf("seconds to wait: %d when absent, %d at most", int(WaitDefault.Seconds()), int(WaitLongest.Seconds()))}, }, "required": []string{"subjects"}}, "description": "Wait for the first event of the named families that matches the filter, and answer it with its " + "family, body and time — or say it timed out. Call it in a loop from a background command, and after every " + "answer read the state from the verb that owns it: an event only wakes you, and one published between two " + "waits is not heard (novox/hq ADR 0367)."} } // waitAnswer answers one mesh_wait call. func (s *Surface) waitAnswer(ctx context.Context, args map[string]any) map[string]any { names, err := waitNames(args["subjects"]) if err != nil { return failure(err.Error()) } timeout, err := waitTimeout(args["timeout"]) if err != nil { return failure(err.Error()) } filter, _ := args["filter"].(map[string]any) if args["filter"] != nil && filter == nil { return failure("mesh_wait's filter is an object of body fields to the values they must equal") } issued := map[string]string{} // name → subject if m := s.conn.Membership(s.conn.Module()); m != nil { for _, w := range m.Waits { issued[w.Event] = w.Subject } } if len(issued) == 0 { return failure("this machine's tool runner was issued no event families to wait on: the mesh grants them from " + "the tool runner's manifest (novox/hq ADR 0367 §2), and its membership names none yet") } byName := make([]string, 0, len(issued)) for name := range issued { byName = append(byName, name) } sort.Strings(byName) family := map[string]string{} // subject → name var subjects []string for _, name := range names { subject, granted := issued[name] if !granted { return failure(fmt.Sprintf("mesh_wait may not wait on %q: this machine's tool runner is granted %s alone. "+ "A family is added only by a reviewed change to the tool runner's manifest (node-tools' module.json, "+ "novox/hq ADR 0367 §6); no argument, call or setting adds one", name, strings.Join(byName, ", "))) } if _, twice := family[subject]; !twice { subjects = append(subjects, subject) } family[subject] = name } heard, ok, err := s.conn.HearFirst(ctx, subjects, timeout, func(body json.RawMessage) bool { return bodyMatches(body, filter) }) if err != nil { return failure("mesh_wait could not wait: " + err.Error()) } if !ok { return answerText(map[string]any{"timed-out": true, "waited": timeout.String(), "subjects": names, "read": "nothing matching was heard; read the state once from the verb that owns it, then wait again"}) } return answerText(map[string]any{ "heard": map[string]any{"subject": family[heard.Subject], "body": heard.Body, "at": heard.At.Format(time.RFC3339Nano)}, "read": "an event only wakes you: read the state from the verb that owns it before acting (novox/hq ADR 0367 §4)", }) } // waitNames is the families a call names: a list of names, or one name. func waitNames(given any) ([]string, error) { var names []string switch v := given.(type) { case string: names = []string{v} case []any: for _, x := range v { name, ok := x.(string) if !ok { return nil, fmt.Errorf("mesh_wait's subjects are event names, such as mesh-knowledge.reviewed; %v is not one", x) } names = append(names, name) } } for i := range names { names[i] = strings.TrimSpace(names[i]) } if len(names) == 0 { return nil, fmt.Errorf("mesh_wait names at least one event family in `subjects`, such as mesh-knowledge.reviewed") } return names, nil } // waitTimeout is the call's timeout: WaitDefault when absent, refused beyond WaitLongest or below a second. func waitTimeout(given any) (time.Duration, error) { if given == nil { return WaitDefault, nil } longest := int(WaitLongest.Seconds()) seconds, ok := given.(float64) if !ok { if n, isInt := given.(int); isInt { seconds, ok = float64(n), true } } if !ok || math.IsNaN(seconds) || seconds < 1 || seconds > float64(longest) { return 0, fmt.Errorf("mesh_wait's timeout is a number of seconds from 1 to %d (novox/hq ADR 0367 §1), not %v", longest, given) } return time.Duration(seconds * float64(time.Second)), nil } // bodyMatches says whether every field the filter names equals, in the event's body, the value given. func bodyMatches(body json.RawMessage, filter map[string]any) bool { if len(filter) == 0 { return true } var fields map[string]json.RawMessage if json.Unmarshal(body, &fields) != nil { return false } for k, want := range filter { got, has := fields[k] if !has { return false } wanted, _ := json.Marshal(want) if !sameJSON(got, wanted) { return false } } return true } // sameJSON compares two JSON values as values: 367 and 367.0 are one number, key order does not matter. func sameJSON(a, b []byte) bool { var x, y any da, db := json.NewDecoder(bytes.NewReader(a)), json.NewDecoder(bytes.NewReader(b)) if da.Decode(&x) != nil || db.Decode(&y) != nil { return false } ja, _ := json.Marshal(x) jb, _ := json.Marshal(y) return bytes.Equal(ja, jb) }