The node-engine speaks operator-channel, mesh-vault and node-engine through their contracts and clients; it serves no pull-waits

ADR 0627 §1, §4: mesh-sdk keeps no asks, asker, vault answers or compose-candidate checks, so the node-engine takes them
from mesh-contracts: operator-channel's contract and client (asks, warrants, the asker's client), mesh-vault's sign verb
and answer, and node-engine's own contract and client for compose-candidate's ask, answer, verdicts and bound. The
lead's ruling on pull-waits (no record gives the seat that verb): the verb and its handler go; the row that read the
waits now holds the seat refusing the verb, and failed before the change. The go.mod requires of mesh-contracts' modules
come with the group's pin.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 14:02:44 +01:00
parent d8207358b0
commit 2b14f166de
14 changed files with 129 additions and 147 deletions
+28 -26
View File
@@ -12,6 +12,8 @@ import (
"sync"
"time"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/agent"
@@ -33,11 +35,11 @@ import (
var errNotReady = errors.New("this node's shadow has not read every input yet")
// candidateWithin is the node's own bound on an answer to compose-candidate: shorter than the checker's
// (delivery.ComposeAnswerBound), because the bus lets the engine answer an ask only within a minute of it (the grants
// (nec.AnswerBound), because the bus lets the engine answer an ask only within a minute of it (the grants
// part's allow_responses ttl, mesh-grants ResponseTTL): an answer at the checker's 60 s bound was refused by the bus
// (issue 1126, 2026-11-03 01:17:04 on shanks, "Permissions Violation for Publish to _INBOX..."), so the checker heard
// nothing at all. A node not done by then answers unready, naming what it waits on.
var candidateWithin = delivery.ComposeAnswerBound - 10*time.Second
var candidateWithin = nec.AnswerBound - 10*time.Second
// maxSentInput is the most composition input an answer carries: the bus carries 1 MiB in one message (its max_payload),
// and the answer's own fields and envelope come beside it.
@@ -92,7 +94,7 @@ func (r *candidateRun) said(now time.Time) (waits string, on string) {
// ComposeCandidate is the node's answer to ask, within candidateWithin (or ctx's deadline, if sooner). Each answer is
// said in one line, with what it waited on and for how long (issue 1126: ace, g14 and shanks answered nothing for a
// minute while nothing said why). An answer not composed within the bound is unready, naming what the node waits on.
func (s *Shadow) ComposeCandidate(ctx context.Context, ask delivery.ComposeCandidateAsk) delivery.ComposeCandidateAnswer {
func (s *Shadow) ComposeCandidate(ctx context.Context, ask nodeengine.ComposeCandidateInput) nodeengine.ComposeCandidateAnswer {
s.mu.Lock()
c := s.c
s.mu.Unlock()
@@ -100,14 +102,14 @@ func (s *Shadow) ComposeCandidate(ctx context.Context, ask delivery.ComposeCandi
ctx, cancel := context.WithTimeout(ctx, candidateWithin)
defer cancel()
run := &candidateRun{}
done := make(chan delivery.ComposeCandidateAnswer, 1)
done := make(chan nodeengine.ComposeCandidateAnswer, 1)
go func() { done <- s.composeCandidate(ctx, c, ask, began, run) }()
var a delivery.ComposeCandidateAnswer
var a nodeengine.ComposeCandidateAnswer
select {
case a = <-done:
case <-ctx.Done():
_, on := run.said(c.now())
a = delivery.ComposeCandidateAnswer{Node: c.env.Node, Candidate: ask.Candidate, Verdict: delivery.ComposeCandidateUnready,
a = nodeengine.ComposeCandidateAnswer{Node: c.env.Node, Candidate: ask.Candidate, Verdict: nec.Unready,
Refused: fmt.Sprintf("the node did not compose the candidate within %s: it waits on %s", candidateWithin, on),
Took: c.now().Sub(began).Milliseconds()}
}
@@ -122,19 +124,19 @@ func (s *Shadow) ComposeCandidate(ctx context.Context, ask delivery.ComposeCandi
}
// composeCandidate composes ask on scratch copies of c, each added to run as it starts.
func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask delivery.ComposeCandidateAsk, began time.Time, run *candidateRun) delivery.ComposeCandidateAnswer {
func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengine.ComposeCandidateInput, began time.Time, run *candidateRun) nodeengine.ComposeCandidateAnswer {
node := c.env.Node
a := delivery.ComposeCandidateAnswer{Node: node, Candidate: ask.Candidate}
refuse := func(why string) delivery.ComposeCandidateAnswer {
a.Verdict, a.Refused, a.Modules, a.Own = delivery.ComposeCandidateRefused, why, nil, delivery.ModuleComposed{}
a := nodeengine.ComposeCandidateAnswer{Node: node, Candidate: ask.Candidate}
refuse := func(why string) nodeengine.ComposeCandidateAnswer {
a.Verdict, a.Refused, a.Modules, a.Own = nec.Refused, why, nil, nodeengine.Module{}
a.Took = c.now().Sub(began).Milliseconds()
return a
}
// A node that cannot compose now (its shadow not ready, a read the bus did not answer, the bound passed) answers
// unready: the checker takes it as no answer (ADR 0593 §9) by the verdict, the words only saying why.
unready := func(why string) delivery.ComposeCandidateAnswer {
unready := func(why string) nodeengine.ComposeCandidateAnswer {
a = refuse(why)
a.Verdict = delivery.ComposeCandidateUnready
a.Verdict = nec.Unready
return a
}
group, heads, err := delivery.ParseCompositionKey(ask.Candidate)
@@ -223,8 +225,8 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask delivery
return unready("a read on the bus did not answer: " + laidErr.Error())
case laidErr != nil && baseErr != nil:
// The node composes none of its own without the candidate either: the candidate changes nothing of that.
a.Verdict, a.Modules = delivery.ComposeCandidateComposed, []delivery.ModuleComposed{}
a.Own = delivery.ModuleComposed{Module: delivery.HaveNodeWord, State: delivery.ModuleHeldState, Words: laidErr.Error()}
a.Verdict, a.Modules = nec.Composed, []nodeengine.Module{}
a.Own = nodeengine.Module{Module: delivery.HaveNodeWord, State: nec.ModuleHeld, Words: laidErr.Error()}
a.Took = c.now().Sub(began).Milliseconds()
return a
case laidErr != nil:
@@ -238,15 +240,15 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask delivery
return out
}
was, now := byModule(baseComps), byModule(laidComps)
one := func(module string) (delivery.ModuleComposed, bool) {
m := delivery.ModuleComposed{Module: module}
one := func(module string) (nodeengine.Module, bool) {
m := nodeengine.Module{Module: module}
if module == "" {
m.Module = delivery.HaveNodeWord
}
if why, held := laidHeld[module]; held {
m.State, m.Words = delivery.ModuleRefusedState, why
m.State, m.Words = nec.ModuleRefused, why
if _, before := baseHeld[module]; before || baseErr != nil {
m.State = delivery.ModuleHeldState
m.State = nec.ModuleHeld
}
return m, true
}
@@ -256,10 +258,10 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask delivery
}
dig, err := delivery.ComposedDigestOf(comp)
if err != nil {
m.State, m.Words = delivery.ModuleRefusedState, err.Error()
m.State, m.Words = nec.ModuleRefused, err.Error()
return m, true
}
m.State, m.Fp, m.Digest = delivery.ModuleComposedState, comp.FP, dig
m.State, m.Fp, m.Digest = nec.ModuleComposed, comp.FP, dig
if before, ok := was[module]; !ok {
m.Changed = true
} else if d, err := delivery.ComposedDigestOf(before); err != nil || d != dig {
@@ -281,24 +283,24 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask delivery
}
}
sort.Strings(names)
a.Modules = []delivery.ModuleComposed{}
a.Modules = []nodeengine.Module{}
refused := false
for _, m := range names {
if mc, ok := one(m); ok {
a.Modules = append(a.Modules, mc)
refused = refused || mc.State == delivery.ModuleRefusedState
refused = refused || mc.State == nec.ModuleRefused
}
}
if own, ok := one(""); ok {
a.Own = own
} else {
a.Own = delivery.ModuleComposed{Module: delivery.HaveNodeWord, State: delivery.ModuleHeldState,
a.Own = nodeengine.Module{Module: delivery.HaveNodeWord, State: nec.ModuleHeld,
Words: "the node composes no composition of its own"}
}
refused = refused || a.Own.State == delivery.ModuleRefusedState
a.Verdict = delivery.ComposeCandidateComposed
refused = refused || a.Own.State == nec.ModuleRefused
a.Verdict = nec.Composed
if refused {
a.Verdict = delivery.ComposeCandidateRefused
a.Verdict = nec.Refused
}
a.Took = c.now().Sub(began).Milliseconds()
return a