J25 A-293: a placed module's failed newest build said as refused in by-entries mode, on the builder's build-ended event

Reading the catalog half by fingerprint, the node-engine no longer reads the builder's builds, so #224's refused key was
never written there and a reader waited on a build that will never be done. The engine now hears the builder's
build.ended stream, read whole at start then followed (as the assigner hears it); the last trunk end of a placed module
that ended failed is written as the refused key with the event's words, a later done end deletes it, an end naming a
branch moves nothing. The manifest consumes mesh-delivery-builder.build.ended.**.

Builds: design 47 G A-293 (with §421, §628), ADR 0593 §6, issue 1966 U1/U8

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 17:20:30 +01:00
parent 4ef07521eb
commit 905265a56b
9 changed files with 312 additions and 8 deletions
+50
View File
@@ -204,6 +204,12 @@ type Composer struct {
// node's own consumer (NoteRunning), under mu; heardRead is whether the first read of the stream ended (RunningRead).
heard map[string]map[string]heardRun
heardRead bool
// ends is, by module, the last build end naming no branch the builder said of it (NoteBuildEnded), its newest build that
// ended, under mu; endsRead is whether the first read of the builder's stream ended (BuildEndsRead) (design 47 G A-293).
ends map[string]builds.BuildEnded
endsRead bool
// endFPs is, by module, every fingerprint a trunk end was heard of: the keys a failed end may have written.
endFPs map[string]map[string]bool
// engine is the node-engine's own module as the last composing of the node's input found it (Scope.Engine), under mu.
engine string
// rehand asks for a pass that hands again what the agent waits for (Waiting).
@@ -2443,6 +2449,50 @@ func (c *Composer) RunningLost() {
c.mu.Unlock()
}
// NoteBuildEnded takes the builder's event build.ended (design 47 G A-293): of a module, the last end naming no branch (the
// trunk's) stands as its newest build that ended, a failed one said as a refusal in the composition state, a done one ending
// that refusal; an end naming a branch moves nothing. Once the first read of the stream ended, an end of a module placed here
// asks a pass.
func (c *Composer) NoteBuildEnded(e builds.BuildEnded) {
if e.Module == "" || e.Fingerprint == "" || e.Branch != "" {
return
}
c.mu.Lock()
if c.ends == nil {
c.ends, c.endFPs = map[string]builds.BuildEnded{}, map[string]map[string]bool{}
}
if c.endFPs[e.Module] == nil {
c.endFPs[e.Module] = map[string]bool{}
}
c.endFPs[e.Module][e.Fingerprint] = true
if had, ok := c.ends[e.Module]; ok && had.Fingerprint == e.Fingerprint && had.Outcome == e.Outcome && had.Error == e.Error {
c.mu.Unlock()
return
}
c.ends[e.Module] = e
ask := c.endsRead
c.mu.Unlock()
if ask {
c.askRehand()
}
}
// BuildEndsRead says the first read of the builder's stream ended: from now the composer acts on the ends it heard, and a
// pass is asked.
func (c *Composer) BuildEndsRead() {
c.mu.Lock()
c.endsRead = true
c.mu.Unlock()
c.askRehand()
}
// BuildEndsLost says the builder's stream is not followed any more: what was heard is not acted on until a new read ends.
func (c *Composer) BuildEndsLost() {
c.mu.Lock()
c.endsRead = false
c.mu.Unlock()
}
func (c *Composer) askRehand() {
select {
case c.rehand <- struct{}{}: