The node's words for a want it composes nothing of are said once per reason, and proved on the live paths (issue 1966)
The said-once keys no longer hold where the indexer's picture stands; a module composed from the manifest for want of a done build is named in Pass.Unmade; the agent's wait is told once per want and again after the composition was there; the two tests are folded into existing ones. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
+17
-5
@@ -246,6 +246,8 @@ type Agent struct {
|
||||
// prior is a module the node ran before this agent started, so it has something to back up.
|
||||
prior map[string]bool
|
||||
said map[string]string
|
||||
// waited is, by module, the want (fingerprint and revision) the agent said it waits for the composer for.
|
||||
waited map[string]string
|
||||
// notComposed is the modules whose have says the node could not compose them: why, and the have before (NotComposed).
|
||||
notComposed map[string]notComposedMark
|
||||
// heldAlone is the modules the composer holds alone (NotComposedEach): what runs of each in the node's own
|
||||
@@ -311,7 +313,7 @@ func New(env Env) *Agent {
|
||||
env.Say = func(string) {}
|
||||
}
|
||||
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]nodeengine.HaveByNodeModuleValue{}, applied: map[string]compNow{}, had: map[string]compNow{}, keptData: map[string]bool{},
|
||||
attempt: map[string]attempt{}, checked: map[string]bool{}, prior: map[string]bool{}, said: map[string]string{}, notComposed: map[string]notComposedMark{},
|
||||
attempt: map[string]attempt{}, checked: map[string]bool{}, prior: map[string]bool{}, said: map[string]string{}, waited: map[string]string{}, notComposed: map[string]notComposedMark{},
|
||||
pulls: map[string]*pullWait{}, settledAt: map[string]time.Time{}, settledSeq: map[string]int{},
|
||||
retry: make(chan []string, 8), reconcile: make(chan []string, 64), failedAgain: make(chan string, 8), tries: map[string]int{}, again: make(chan againAsk, 64),
|
||||
transient: make(chan struct{}, 1), stopped: make(chan struct{}), quiet: make(chan chan struct{})}
|
||||
@@ -1327,7 +1329,7 @@ func (a *Agent) HoldsNode() bool {
|
||||
// waitsForComposer says once per want and reason that the agent waits for the node's own composition of module at fp, and
|
||||
// why the composer made none (Env.WhyUnmade), and tells the composer (Env.Waiting) so it hands again what it believes it
|
||||
// handed. Called without a.mu held.
|
||||
func (a *Agent) waitsForComposer(module, fp string) {
|
||||
func (a *Agent) waitsForComposer(module, fp string, rev uint64) {
|
||||
why := "the composer has made none yet"
|
||||
if a.env.WhyUnmade != nil {
|
||||
if w := a.env.WhyUnmade(module); w != "" {
|
||||
@@ -1335,9 +1337,14 @@ func (a *Agent) waitsForComposer(module, fp string) {
|
||||
}
|
||||
}
|
||||
line := fmt.Sprintf("%s waits for the composer: no composition of it at %s: %s", module, fp, why)
|
||||
// Said and told once per want (its fingerprint and revision), whatever the reason says; forgotten once composed.
|
||||
at := fmt.Sprintf("%s@%d", fp, rev)
|
||||
a.mu.Lock()
|
||||
first := a.said["composer:"+module] != line
|
||||
a.sayOnce("composer:"+module, line)
|
||||
first := a.waited[module] != at
|
||||
if first {
|
||||
a.waited[module] = at
|
||||
a.env.Say(line)
|
||||
}
|
||||
a.mu.Unlock()
|
||||
if first && a.env.Waiting != nil {
|
||||
a.env.Waiting(module, fp)
|
||||
@@ -1359,9 +1366,14 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
|
||||
comp, have := a.compositionFor(module, w.Want.Fp)
|
||||
a.mu.Unlock()
|
||||
if a.env.SelfComposed {
|
||||
if have {
|
||||
a.mu.Lock()
|
||||
delete(a.waited, module)
|
||||
a.mu.Unlock()
|
||||
}
|
||||
if !have || comp.Err != nil {
|
||||
if !have {
|
||||
a.waitsForComposer(module, w.Want.Fp)
|
||||
a.waitsForComposer(module, w.Want.Fp, w.Rev)
|
||||
}
|
||||
return // waits for the node's own composition of the want's build
|
||||
}
|
||||
|
||||
@@ -1556,6 +1556,11 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
history[key] = append(history[key], Update{Key: key, Value: raw, Revision: uint64(len(history[key]) + 1)})
|
||||
}
|
||||
b.env.Held = func() map[string]string { return map[string]string{"mesh-build-go": "its binding is unset"} }
|
||||
// Issue 1966 (novox, 2026-11-09): a want the composer made no composition of is said once, with why, and the composer told.
|
||||
var toldMu sync.Mutex
|
||||
var told []string
|
||||
b.env.WhyUnmade = func(string) string { return "no manifest in the composer's picture" }
|
||||
b.env.Waiting = func(module, fp string) { toldMu.Lock(); told = append(told, module+"@"+fp); toldMu.Unlock() }
|
||||
stop := b.run()
|
||||
defer stop()
|
||||
unsigned := func(module, fp string, rs ...json.RawMessage) (string, []byte, string) {
|
||||
@@ -1583,6 +1588,33 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
b.comps.put(ck, craw)
|
||||
want("web", fp, "")
|
||||
eventually(t, "the node's own composition was not applied", func() bool { return len(b.appliedList()) == 1 })
|
||||
gk := meshwants.WantsByWantNodeModuleKey(node, "ghost")
|
||||
graw, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fpOf("9"), Self: true})
|
||||
b.wants.put(gk, graw)
|
||||
line := "ghost waits for the composer: no composition of it at " + fpOf("9") + ": no manifest in the composer's picture"
|
||||
saidCount := func() (n int) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, l := range b.said {
|
||||
if l == line {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
eventually(t, "the wait for the composer was not said", func() bool { return saidCount() == 1 })
|
||||
// Settled again by another want's change, it is neither said nor told again.
|
||||
want("web", fp, "")
|
||||
b.wants.put(gk, graw)
|
||||
eventually(t, "the composer was not told", func() bool { toldMu.Lock(); defer toldMu.Unlock(); return len(told) == 1 })
|
||||
if _, err := b.ag.Composition(context.Background(), "ghost", "", false); err == nil || !strings.Contains(err.Error(), "waits for the composer: no manifest in the composer's picture") {
|
||||
t.Errorf("the composition read of ghost says %v, want why the composer made none", err)
|
||||
}
|
||||
toldMu.Lock()
|
||||
if saidCount() != 1 || len(told) != 1 || told[0] != "ghost@"+fpOf("9") {
|
||||
t.Errorf("said %d times, told %q; want once and ghost told once", saidCount(), told)
|
||||
}
|
||||
toldMu.Unlock()
|
||||
eventually(t, "the have does not name the composed digest", func() bool { return b.haves.last("web").Composed == dig })
|
||||
// ADR 0601 §4: what the composer keeps of web as it runs is carried on its have, and goes once it keeps none.
|
||||
b.ag.KeptNow(context.Background(), map[string][]string{"web": {"web runs the tool runner's process here"}})
|
||||
@@ -1935,23 +1967,3 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone(
|
||||
t.Fatalf("a failed apply was tried again with no edge: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1966 (novox, 2026-11-09): an agent that waits for its node's own composition of a want says so once, with why the
|
||||
// composer made none, and tells the composer, which hands again what it believes it handed.
|
||||
func TestAnAgentWaitingForTheComposerSaysSoOnceAndTellsIt(t *testing.T) {
|
||||
b := newBed(t)
|
||||
b.env.SelfComposed = true
|
||||
var said []string
|
||||
b.env.Say = func(s string) { said = append(said, s) }
|
||||
b.env.WhyUnmade = func(string) string { return "no manifest in the composer's picture" }
|
||||
var told []string
|
||||
b.env.Waiting = func(module, fp string) { told = append(told, module+"@"+fp) }
|
||||
a := New(b.env)
|
||||
for range 2 {
|
||||
a.waitsForComposer("web", fpOf("a"))
|
||||
}
|
||||
want := "web waits for the composer: no composition of it at " + fpOf("a") + ": no manifest in the composer's picture"
|
||||
if len(said) != 1 || said[0] != want || len(told) != 1 || told[0] != "web@"+fpOf("a") {
|
||||
t.Errorf("said %q, told %q; want %q once and web told once", said, told, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user