node-tools: a tool call names its caller, and the module is told it from the subject (hq issue 365)
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery-group group fix/365-a-tool-call-names-its-caller delivering: 0 of 4 delivered
mesh/delivery superseded: a newer head of the same pull request

Each tool and seat verb is also served on its caller-named subject, kind call
with the caller's bus user last, which the controller grants each user with its
own name alone; the runtime hands the module that caller in the call's _meta,
never from the body. Calls through the runtime (the console and modules'
invokes) ask the caller-named subject first and the old one only when the first
reached nobody: nothing serves it yet, or the bus refused it because the grant
is not delivered yet, which ends the call at once rather than after 30 s. The
old subjects stay served and say no caller for one release (hq issue 464).
This commit is contained in:
2026-10-11 05:19:08 +02:00
parent 6a6ed725c0
commit ab0f888d67
9 changed files with 642 additions and 13 deletions
+74 -5
View File
@@ -5,6 +5,8 @@
// mesh.mod.<module>.event.<type> an event a module emits
// mesh.mod.<module>.tool.<tool> a tool a module serves
// mesh.seat.<seat>.tool.<verb> a role's verb, answered by whoever holds the seat
// mesh.mod.<module>.call.<tool>.<caller>, mesh.seat.<seat>.call.<verb>.<caller>
// the same, naming the bus user that called (caller.go, hq issue 365)
package bus
import (
@@ -170,6 +172,8 @@ type Conn struct {
// RetryAfter is how long after a refusal a subscription is asked for again, by attempt; past the
// last, it is given up and said. A variable so a test need not wait minutes.
RetryAfter []time.Duration
// calls are the caller-named calls in flight (caller.go, novox/hq issue 365).
calls callers
}
// answered is one subject the runtime answers on: what to subscribe again with, and how often it was
@@ -204,6 +208,9 @@ func (c *Conn) refused(err error) {
if !errors.Is(err, nats.ErrPermissionViolation) {
return
}
if c.publishRefused(err) {
return
}
m := refusedSubject.FindStringSubmatch(err.Error())
if len(m) < 2 {
return
@@ -441,10 +448,17 @@ type reply struct {
// answerOn answers one subject with one handler, and says which machine answered (ADR 0159).
func (c *Conn) answerOn(subject, queue string, h Handler) (func(), error) {
return c.answerCalledOn(subject, queue, func(_ string, body json.RawMessage) (any, error) { return h(body) })
}
// answerCalledOn answers one subject, handing the handler the caller the subject a call arrived on names:
// under a pattern calledPattern made, its last token; on any other subject, "" — none was proven (novox/hq
// issue 365). Never from the body: a body is the caller's to write.
func (c *Conn) answerCalledOn(subject, queue string, h CalledHandler) (func(), error) {
cb := func(msg *nats.Msg) {
go func() {
var r reply
result, err := h(json.RawMessage(msg.Data))
result, err := h(callerOn(subject, msg.Subject), json.RawMessage(msg.Data))
if err != nil {
r.Error = err.Error()
} else {
@@ -497,18 +511,50 @@ func nullable(v any) any {
// HandleSubject answers a subject outright: a seat's verb where the mesh issued it.
func (c *Conn) HandleSubject(subject string, h Handler) (func(), error) {
return c.answerOn(subject, "", h)
return c.HandleSubjectCalled(subject, func(_ string, body json.RawMessage) (any, error) { return h(body) })
}
// HandleSubjectCalled answers a tool subject and its caller-named counterpart (novox/hq issue 365): a call on
// the counterpart is handed the caller its subject names, one on the subject itself "" — none proven. The
// subject that names no caller is served beside the other for one release, so callers move without a gap
// (its retirement: hq issue 464).
func (c *Conn) HandleSubjectCalled(subject string, h CalledHandler) (func(), error) {
return c.answerBoth(subject, "", h)
}
// answerBoth answers a subject and, when it is a tool's, the caller-named pattern beside it, in one queue.
func (c *Conn) answerBoth(subject, queue string, h CalledHandler) (func(), error) {
stop, err := c.answerCalledOn(subject, queue, h)
if err != nil {
return func() {}, err
}
pattern := calledPattern(subject)
if pattern == "" {
return stop, nil
}
stopCalled, err := c.answerCalledOn(pattern, queue, h)
if err != nil {
c.Logf("[mesh-tools] cannot serve the caller-named %s: %v; its callers are asked on %s alone", pattern, err, subject)
return stop, nil
}
return func() { stop(); stopCalled() }, nil
}
// Handle serves `<module>.<tool>` where the mesh issued that module, and follows its membership:
// when a new one arrives, it serves where it now says and stops where it no longer does.
func (c *Conn) Handle(key string, h Handler) (func(), error) {
return c.HandleCalled(key, func(_ string, body json.RawMessage) (any, error) { return h(body) })
}
// HandleCalled is Handle telling the handler who called: the bus user a caller-named subject names, or "" for
// a call on the subject that names none (novox/hq issue 365).
func (c *Conn) HandleCalled(key string, h CalledHandler) (func(), error) {
if strings.HasPrefix(key, "seat:") {
subject, err := ToolSubject(key, c.self)
if err != nil {
return func() {}, err
}
return c.answerOn(subject, "", h)
return c.answerBoth(subject, "", h)
}
module, tool := c.self, key
if dot := strings.Index(key, "."); dot >= 0 {
@@ -528,7 +574,7 @@ func (c *Conn) Handle(key string, h Handler) (func(), error) {
}
stops = nil
for _, s := range c.servedOn(module, tool) {
if stop, err := c.answerOn(s.Subject, s.Queue, h); err == nil {
if stop, err := c.answerBoth(s.Subject, s.Queue, h); err == nil {
stops = append(stops, stop)
} else {
c.Logf("[mesh-tools] cannot serve %s on %s: %v", key, s.Subject, err)
@@ -585,6 +631,23 @@ func (c *Conn) Ask(key string, body any, on string) (Answered, error) {
if err != nil {
return Answered{}, err
}
// **Named by its caller first** (novox/hq issue 365): on the caller-named subject, which the bus lets this
// account publish with its own name alone; asked again on the subject that names no caller only when the
// first reached nobody — nothing serves it yet, or the bus does not grant it yet — so no call is done twice.
if called := CalledSubject(subject, c.cred.User); called != "" {
answered, handingOver, fallBack, err := c.askCalled(called, data)
if !fallBack {
if !handingOver {
return answered, err
}
time.Sleep(HandoverPause)
answered, again, _, err := c.askCalled(called, data)
if err != nil && again {
return Answered{}, fmt.Errorf("asked twice, %s apart, and both times refused as a handover: %w", HandoverPause, err)
}
return answered, err
}
}
answered, handingOver, err := c.askOnce(subject, data)
if !handingOver {
return answered, err
@@ -621,8 +684,14 @@ func (c *Conn) askOnce(subject string, data []byte) (Answered, bool, error) {
}
return Answered{}, false, err
}
return c.answerOf(subject, msg.Data)
}
// answerOf reads one answer, asking the rest of a paged one on the subject it was asked on, and says whether it
// was a refusal marked to be asked again.
func (c *Conn) answerOf(subject string, data []byte) (Answered, bool, error) {
// An answer larger than one message comes in parts (pages.go, novox/hq issue 314).
body, err := c.whole(subject, msg.Data)
body, err := c.whole(subject, data)
if err != nil {
return Answered{}, false, err
}