Merge fix/compose-pass-bounded: the composition pass never waits on marking haves

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 22:35:09 +01:00
5 changed files with 229 additions and 3 deletions
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"context"
"sync"
"time"
)
// markWithin bounds one marking of the haves (what the composer tells its agent: a refusal, the words it keeps). The marks
// read and write each module's have over the bus one after the other; a bus that answers slowly or not at all must hold
// the marking, never the composition pass (issue 1965, shanks 2026-11-05 22:22: a pass waited minutes on them).
var markWithin = 45 * time.Second
// marker runs the composer's marks of the haves off the composition pass: set returns at once, one worker runs what was
// set last under each name, in the order the names were first set, each within markWithin. A mark set while one runs
// replaces the pending mark of its name, so the haves are marked once for the newest word, not once for every pass.
type marker struct {
run context.Context
mu sync.Mutex
pending map[string]func(context.Context)
order []string
working bool
idle *sync.Cond
}
func newMarker(run context.Context) *marker {
m := &marker{run: run, pending: map[string]func(context.Context){}}
m.idle = sync.NewCond(&m.mu)
return m
}
// set takes the mark name should carry; it never waits on the bus.
func (m *marker) set(name string, mark func(context.Context)) {
m.mu.Lock()
defer m.mu.Unlock()
if _, queued := m.pending[name]; !queued {
m.order = append(m.order, name)
}
m.pending[name] = mark
if m.working {
return
}
m.working = true
go m.work()
}
func (m *marker) work() {
for {
m.mu.Lock()
if len(m.order) == 0 || m.run.Err() != nil {
m.order, m.pending = nil, map[string]func(context.Context){}
m.working = false
m.idle.Broadcast()
m.mu.Unlock()
return
}
name := m.order[0]
m.order = m.order[1:]
mark := m.pending[name]
delete(m.pending, name)
m.mu.Unlock()
ctx, cancel := context.WithTimeout(m.run, markWithin)
mark(ctx)
cancel()
}
}
// wait returns once no mark is pending or running (for tests).
func (m *marker) wait() {
m.mu.Lock()
defer m.mu.Unlock()
for m.working {
m.idle.Wait()
}
}
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"context"
"testing"
"time"
)
// Issue 1965 (shanks 2026-11-05 22:22): the marks of the haves never hold the composition pass. A mark that stalls on the
// bus is cut at markWithin, a mark set while one runs replaces the pending one of its name, and set itself never waits.
func TestMarksOfTheHavesNeverHoldThePass(t *testing.T) {
setMarkWithin(t, 30*time.Millisecond)
m := newMarker(context.Background())
var ran []string
stalled := make(chan struct{})
began := time.Now()
m.set("refused", func(ctx context.Context) { // a have write that stalls until its bound
close(stalled)
<-ctx.Done()
ran = append(ran, "refused-stalled")
})
<-stalled
// Set while the first runs: only the newest of a name is kept.
m.set("kept", func(context.Context) { ran = append(ran, "kept-old") })
m.set("kept", func(context.Context) { ran = append(ran, "kept-new") })
m.set("refused", func(context.Context) { ran = append(ran, "refused-new") })
if took := time.Since(began); took > 20*time.Millisecond {
t.Errorf("setting marks took %v; it must not wait on the stalled one", took)
}
m.wait()
want := []string{"refused-stalled", "kept-new", "refused-new"}
if len(ran) != len(want) {
t.Fatalf("ran %v; want %v", ran, want)
}
for i := range want {
if ran[i] != want[i] {
t.Fatalf("ran %v; want %v", ran, want)
}
}
}
func setMarkWithin(t *testing.T, d time.Duration) {
t.Helper()
old := markWithin
markWithin = d
t.Cleanup(func() { markWithin = old })
}
+5 -2
View File
@@ -596,6 +596,8 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
// No reader is no gate that lets everything through: the node acts on nothing new, as one that waits does.
say(err.Error() + "; the node composes nothing new until it is started again and reads the list (novox/hq ADR 0636 §1)")
}
// The marks of the haves run off the composition pass, each bounded (see marker).
marks := newMarker(run)
hooks := composerHooks{
seatList: seats,
seatListFailed: err != nil,
@@ -621,13 +623,14 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
},
refused: func(why string, held map[string]string) {
if now := own.get(); now != nil && run.Err() == nil {
now.NotComposedEach(run, why, held) // a module held alone marks that module, never the others (issue 1126)
// A module held alone marks that module, never the others (issue 1126); the marking never holds the pass.
marks.set("refused", func(ctx context.Context) { now.NotComposedEach(ctx, why, held) })
}
},
// And what it keeps of a module as it runs, for a manifest that does not say it yet: on the module's have (ADR 0601 §4).
kept: func(kept map[string][]string) {
if now := own.get(); now != nil && run.Err() == nil {
now.KeptNow(run, kept)
marks.set("kept", func(ctx context.Context) { now.KeptNow(ctx, kept) })
}
},
// And the vault's authority beside the controller's, and this node's certificate asked of the vault when a