Compare commits

...
Author SHA1 Message Date
jschoubben f074fb8df4 messenger: say each ask taken on the bus as operator-channel.asked.<asker>, so a session hears an approval request open
The operator, 2026-10-30: "you should be able to hook into approval requests so you also get a notification". The router said only an ask's end (decided.<asker>); it now says every ask it takes, open or held, on the seat's event asked.<asker>: its id, asker, about, headline, state and expiry, never its bindings or whole words. The seat declares asked among its events and names it by caller, as decided. The subject is named in the module until the SDK's asks contract carries it.
2026-10-30 11:44:22 +01:00
jschoubben 10789ce9fd mesh-delivery-gate: an Approve is acted on or said within 60 s, on the bus and as an urgent condition
On 2026-10-30 the group feat/774-l3-reland was approved at 10:14 and nothing was handed for an hour with no word: six members kept a Decline of the earlier ask beside the new Approve, because the fix of 2026-10-29 (add6f143, cherry-picked before this commit) was reverted with L3 in c8240c38. Beside that fix, every Approve is now said as mesh-delivery-gate.approval {state: approved}, and an approved merge whose pass has not started 60 s after it is said once as {state: not-handed} with what it waits on and is an urgent stalled line until it starts (the operator: "you should know about it immediately"). One table test, two rows: a Decline kept before the Approve merges; a merge that cannot start is said within its bound.
2026-10-30 11:40:09 +01:00
jschoubben d6a069ae49 mesh-delivery-gate: an answer stands only for the ask it answers, and a merge never handed says why
mesh/delivery merged: source.main moved to novox/mesh-catalog@1aa22efb0d78
mesh/check being carried to d6a069ae4959 on 1aa22efb0d78 | from 12c2574c9d54
On 2026-10-29 the group feat/774-vault-mints-l2 was approved and never handed: hq#495 kept a Decline of an earlier ask beside the Approve of the next, readyAsks skipped it and handTurn returned nil for every member, with no log line. The Approve and a new ask now clear an earlier Decline, a run's answer names its ask (answered_ask), an approved merge that waits for nothing and is not handed logs why once and is a stalled line, and delivery_turns lists no unwritten turn while the check gates no merge.
2026-10-30 11:35:35 +01:00
16 changed files with 480 additions and 28 deletions
@@ -330,7 +330,7 @@ func (h *Holder) endStaleAsks() {
h.endStaleSettles()
for _, d := range h.deliveries {
r := d.Ready
if r == nil || r.Merged != nil || d.MergedAs != "" || r.Declined != nil || (r.AskID == "" && r.Approved == nil) || len(r.Heads) == 0 {
if r == nil || r.Merged != nil || d.MergedAs != "" || r.declined() || (r.AskID == "" && r.Approved == nil) || len(r.Heads) == 0 {
continue
}
switch {
@@ -418,6 +418,7 @@ func (h *Holder) approval(d *Delivery, now time.Time) (string, func()) {
h.runOf(m, now)
m.Ready.AskID, m.Ready.Ask, m.Ready.Heads, m.Ready.AskedAt, m.Ready.Asks = id, &ask, heads, now, max(m.Ready.Asks, r.Asks)
m.Ready.AskedKey, m.Ready.Asking, m.Ready.AskCheck = id, asking, check.Key
m.Ready.Declined, m.Ready.AnsweredAsk = nil, "" // a new ask: an answer to an earlier one does not stand on it
h.keep(m)
taken = append(taken, m)
reads = append(reads, readOf(m, m.Commit))
@@ -608,7 +609,7 @@ func (h *Holder) pendingAsks() []string {
seen := map[string]bool{}
var out []string
for _, d := range h.deliveries {
if r := d.Ready; r != nil && r.AskID != "" && r.Approved == nil && r.Declined == nil && !seen[r.AskID] {
if r := d.Ready; r != nil && r.AskID != "" && r.Approved == nil && !r.declined() && !seen[r.AskID] {
seen[r.AskID] = true
out = append(out, r.AskID)
}
@@ -705,16 +706,19 @@ func (h *Holder) answered(key string, ans sdk.Answer) []func() {
h.mergeAgainNow(d) // the operator chose: the merge is asked at once
switch ans.Option {
case OptionApprove:
r.Approved = &now
r.Approved, r.Declined, r.AnsweredAsk = &now, nil, key
h.Logf("[mesh-delivery] %s: the operator approved its merge into main (%s)", d.ID, r.Warrant)
case OptionDecline:
r.Declined = &now
r.Declined, r.AnsweredAsk = &now, key
h.Logf("[mesh-delivery] %s: the operator declined its merge into main (%s)", d.ID, r.Warrant)
said = append(said, h.sayOn(d, fmt.Sprintf("mesh-delivery: **not merged.** The merge of this pull request into main at %s was "+
"declined: %s. A new head asks the operator again.", d.Commit, r.Warrant)))
}
h.keep(d)
}
if ans.Option == OptionApprove {
h.oweApproved(covered[0], now) // said on the bus at once, and again at its bound if its merge has not started (unacted.go)
}
return said
}
@@ -1,6 +1,7 @@
package main
import (
"fmt"
"strings"
"testing"
"time"
@@ -405,3 +406,140 @@ func TestAnAnswerToAnEarlierAskingIsNotTakenAndTheMergeNamesTheAnsweredAsking(t
t.Fatalf("the merge is announced %v; wanted the ask id of asking 2, %s", say, sdk.AskingID(key, 2))
}
}
// 2026-10-29 (hq#495): a group declined under one ask, a member pushed, and the group asked and approved again at the new
// heads: the member whose head did not move kept the earlier Decline beside the new Approve, and nothing was handed, with
// no word. An answer stands only for the ask it answers; whichever member is pushed, both merge on the new Approve.
func TestADeclineOfAnEarlierAskDoesNotHoldTheApproveOfTheNext(t *testing.T) {
const ctlHead, catHead, pushed = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3"
for _, push := range []struct {
repo string
number int
trunk string
}{{"novox/mesh-catalog", 2, "t3t3t3t3t3t3"}, {"novox/mesh-controller", 1, "t1t1t1t1t1t1"}} {
t.Run(push.repo, func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
heads := map[string]string{"novox/mesh-controller": ctlHead, "novox/mesh-catalog": catHead}
paths := map[string]string{"novox/mesh-controller": "internal/a.go", "novox/mesh-catalog": "modules/app/module.json"}
numbers := map[string]int{"novox/mesh-controller": 1, "novox/mesh-catalog": 2}
trunks := map[string]string{"novox/mesh-controller": "t1t1t1t1t1t1", "novox/mesh-catalog": "t3t3t3t3t3t3"}
round := func(check string) {
var open []OpenPull
for _, repo := range []string{"novox/mesh-controller", "novox/mesh-catalog"} {
w.h.PullUpdated(pr(repo, numbers[repo], heads[repo], "feat/x", paths[repo]))
w.h.Checked(judgedAt(repo, numbers[repo], heads[repo], "pass", trunks[repo]))
open = append(open, openPull(repo, numbers[repo], heads[repo]))
}
w.forge.open = open
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: check, Verdict: "pass", Summary: "both compose"})
w.settleAll()
}
round("check-1")
w.op.answer(w.op.open()[0], OptionDecline)
w.settleAll()
heads[push.repo] = pushed
round("check-2")
open := w.op.open()
if len(open) != 1 {
t.Fatalf("the group at its new heads is not asked once: %v", open)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("approved at the new heads, yet merged %v; stalled %+v", w.forge.merges, w.h.Stalled())
}
})
}
}
// 2026-10-30 (feat/774-l3-reland): approved at 10:14 and never handed, with no word. Each row an Approve: one whose runs
// were kept by a gate before the fix above, a Decline of an earlier ask beside the Approve, merges; one whose merge
// cannot start is said on the bus (mesh-delivery-gate.approval) and as an urgent line within approvalBound, and merges
// once what held it ends.
func TestAnApproveIsActedOnOrSaidWithinItsBound(t *testing.T) {
for _, row := range []struct {
name string
kept bool // the runs carry a Decline of an earlier ask, kept before answers named their ask
forgeDown bool // the members' statuses cannot be read, so the pass cannot start
}{
{name: "a Decline kept before the Approve, as the gate kept it before answers named their ask", kept: true},
{name: "an Approve whose merge cannot start is said within its bound", forgeDown: true},
} {
t.Run(row.name, func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
heads := map[string]string{"novox/mesh-controller": "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "novox/mesh-catalog": "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"}
paths := map[string]string{"novox/mesh-controller": "internal/a.go", "novox/mesh-catalog": "modules/app/module.json"}
numbers := map[string]int{"novox/mesh-controller": 1, "novox/mesh-catalog": 2}
trunks := map[string]string{"novox/mesh-controller": "t1t1t1t1t1t1", "novox/mesh-catalog": "t3t3t3t3t3t3"}
var open []OpenPull
for _, repo := range []string{"novox/mesh-controller", "novox/mesh-catalog"} {
w.h.PullUpdated(pr(repo, numbers[repo], heads[repo], "feat/x", paths[repo]))
w.h.Checked(judgedAt(repo, numbers[repo], heads[repo], "pass", trunks[repo]))
open = append(open, openPull(repo, numbers[repo], heads[repo]))
}
w.forge.open = open
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
asked := w.op.open()
if len(asked) != 1 {
t.Fatalf("the group is not asked once: %v", asked)
}
w.op.answer(asked[0], OptionApprove)
w.forge.mu.Lock()
w.forge.down = row.forgeDown
w.forge.mu.Unlock()
w.h.hearAsks()
if row.kept {
w.h.mu.Lock()
for _, d := range w.h.deliveries {
if d.Ready != nil && d.Ready.Approved != nil {
before := d.Ready.Approved.Add(-38 * time.Minute)
d.Ready.Declined, d.Ready.AnsweredAsk = &before, ""
}
}
w.h.mu.Unlock()
}
w.settleAll()
w.later(approvalBound + time.Second)
w.settleAll()
states := map[string]bool{}
for _, e := range w.said {
if e["event-kind"] == approvalEvent && e["group"] == "feat/x" {
states[fmt.Sprint(e["state"])] = true
}
}
if !states["approved"] {
t.Fatalf("the Approve is not said on the bus: %v", w.said)
}
var urgent []StalledLine
for _, l := range w.h.Stalled() {
if l.Bound == approvalBound.String() && l.Severity == "urgent" && strings.Contains(l.Says, "feat/x was approved") {
urgent = append(urgent, l)
}
}
if !row.forgeDown {
if len(w.forge.merges) != 2 || states["not-handed"] || len(urgent) != 0 {
t.Fatalf("approved, yet merged %v (said not handed: %v, lines %+v)", w.forge.merges, states["not-handed"], urgent)
}
return
}
if len(w.forge.merges) != 0 || !states["not-handed"] || len(urgent) != 1 {
t.Fatalf("an Approve held past its bound: merged %v, said not handed %v, urgent lines %+v", w.forge.merges, states["not-handed"], urgent)
}
w.forge.mu.Lock()
w.forge.down = false
w.forge.mu.Unlock()
w.later(askAgainAfter)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("the forge back, yet merged %v; stalled %+v", w.forge.merges, w.h.Stalled())
}
})
}
}
@@ -96,7 +96,7 @@ func (h *Holder) withdrawForStatus(e ForgeStatusEvent) {
done := map[string]bool{}
for _, d := range h.deliveries {
r := d.Ready
if !strings.EqualFold(d.Repository, repository) || r == nil || r.AskID == "" || r.Approved != nil || r.Declined != nil || done[r.AskID] {
if !strings.EqualFold(d.Repository, repository) || r == nil || r.AskID == "" || r.Approved != nil || r.declined() || done[r.AskID] {
continue
}
head := d.Commit
@@ -208,7 +208,7 @@ func (h *Holder) passes() []pass {
for _, id := range ids {
d := h.deliveries[id]
r := d.Ready
if r == nil || r.Approved == nil || r.Declined != nil || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
if r == nil || r.Approved == nil || r.declined() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
d.Base != MergeBranch || h.allMerged(r.Heads) {
continue
}
@@ -335,7 +335,7 @@ func (h *Holder) MergeAgain(id, why string) (string, error) {
return "", fmt.Errorf("say why: the merge asked again is the operator's word, kept in the log")
}
c := h.covering(d)
if c == nil || c.Approved == nil || c.Declined != nil || c.Heads[d.ID] != d.Commit {
if c == nil || c.Approved == nil || c.declined() || c.Heads[d.ID] != d.Commit {
return "", fmt.Errorf("%s is not approved at its head %s: nothing to ask again (a new head is asked on its own)", id, shortOf(d.Commit, 12))
}
if d.MergedAs != "" || (d.Ready != nil && d.Ready.Merged != nil) {
@@ -524,7 +524,7 @@ func (h *Holder) PullEdited(p PullEvent) {
if after, _ := reviewWords(d); after == before {
return
}
if r := h.covering(d); r != nil && r.AskID != "" && r.Approved == nil && r.Declined == nil {
if r := h.covering(d); r != nil && r.AskID != "" && r.Approved == nil && !r.declined() {
h.cancels = append(h.cancels, h.withdrawAsk(r.AskID, d.ID+"'s review record changed: "+shortOf(d.ReviewedAt, 12)))
}
}
@@ -89,7 +89,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
if !reflect.DeepEqual(m.Consumes, followed) || !slices.Contains(m.Consumes, "mesh-delivery-forge-bridge.forge.**") {
t.Fatalf("consumes %v, follows %v (the bridge's forge messages among them)", m.Consumes, followed)
}
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged"}) {
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged", "approval"}) {
t.Fatalf("emits %v", m.Emits)
}
// Nine buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3, the conditions it raised (issue 1275) and the
@@ -149,11 +149,10 @@ func (h *Holder) handTurn(g *Group, d *Delivery, now time.Time) func() {
return nil
}
for _, m := range order {
mc := h.covering(m)
if m.Ready == nil || m.Ready.Declined != nil || mc == nil || mc.Approved == nil || mc.Heads[m.ID] != m.Commit || h.readyWaits(m) != "" {
return nil // the others are not approved at their heads yet: nothing is merged before they are
if h.notHandable(m) != "" {
return nil // the others are not approved at their heads yet: nothing is merged before they are (said by notHanded)
}
reads = append(reads, readOf(m, mc.Heads[m.ID]))
reads = append(reads, readOf(m, h.covering(m).Heads[m.ID]))
}
}
if c == nil || c.Approved == nil || c.Heads[d.ID] == "" {
@@ -169,6 +168,66 @@ func (h *Holder) handTurn(g *Group, d *Delivery, now time.Time) func() {
return func() { h.hand(gid, name, key, target, reads, approved) }
}
// notHandable is why a group member cannot be handed in its group's pass, empty when it can: handTurn's own test, in words.
// Under the lock.
func (h *Holder) notHandable(m *Delivery) string {
mc := h.covering(m)
switch {
case m.Ready == nil:
return m.ID + " has no ready run (" + string(m.State) + ")"
case m.Ready.declined():
return m.ID + " stands declined since " + m.Ready.Declined.UTC().Format(time.RFC3339)
case mc == nil || mc.Approved == nil:
return m.ID + " is not approved at its head " + shortOf(m.Commit, 12)
case mc.Heads[m.ID] != m.Commit:
return m.ID + "'s head " + shortOf(m.Commit, 12) + " is not the head approved"
}
if w := h.readyWaits(m); w != "" {
return m.ID + " waits: " + w
}
return ""
}
// notHanded is why the approved merge of d, which waits for nothing, is not handed: its group's pass cannot start, and
// says which member holds it and why. Empty when nothing holds it, or when the pass is under way or its refusal is said
// already. Under the lock.
func (h *Holder) notHanded(g *Group, d *Delivery) string {
if g == nil {
return ""
}
order := h.unmergedLive(g)
if len(order) == 0 {
return ""
}
if first := order[0].Ready; first != nil && (first.merging || !first.LastTry.IsZero() || first.RefusedSince != nil) {
return "" // the pass is under way, or its refusal is said on its own line
}
for _, m := range order {
if why := h.notHandable(m); why != "" {
return fmt.Sprintf("its group %s is not handed to the forge bridge: %s", g.ID, why)
}
}
return ""
}
// noteNotHanded keeps on d's run why its approved merge is not handed, said once in the log when it changes, and as a
// stalled line meanwhile (readyLines). Under the lock.
func (h *Holder) noteNotHanded(d *Delivery, why string, now time.Time) {
r := d.Ready
if r == nil || r.NotHanded == why {
return
}
r.NotHanded = why
if why == "" {
r.NotHandedSince = nil
} else {
at := now
r.NotHandedSince = &at
h.Logf("[mesh-delivery] %s is approved, waits for nothing, and is NOT handed to merge: %s", d.ID, why)
}
h.keep(d)
}
// hand reads a group's required statuses and, when all are a success, writes the target as the member being merged.
func (h *Holder) hand(gid, name, key, target string, reads []memberRead, approved map[string]string) {
if gid != "" && !h.groupMergeable(gid, target, reads) {
@@ -120,11 +120,13 @@ type ReadyRun struct {
// in new words, so it never keeps a claim the check no longer holds.
AskCheck string `json:"ask_check,omitempty"`
// Approved and Declined are when the operator chose, Warrant what the router's warrant says of who and how; Refused
// why an answer was not taken (it did not hold to the ask).
Approved *time.Time `json:"approved,omitempty"`
Declined *time.Time `json:"declined,omitempty"`
Warrant string `json:"warrant,omitempty"`
Refused string `json:"refused,omitempty"`
// why an answer was not taken (it did not hold to the ask). AnsweredAsk is the ask the answer answers: an answer stands
// only for its own ask, so a Decline of an earlier ask never stands beside the Approve of a later one (declined).
Approved *time.Time `json:"approved,omitempty"`
Declined *time.Time `json:"declined,omitempty"`
AnsweredAsk string `json:"answered_ask,omitempty"`
Warrant string `json:"warrant,omitempty"`
Refused string `json:"refused,omitempty"`
// One check per delivery (novox/hq ADR 0548 §2, §4, turns.go): HeldSince is when the approved merge began to wait for the
// check of other heads HeldFor names (its verdict key), running on a repository it goes into; HandNow the operator's word
@@ -137,6 +139,28 @@ type ReadyRun struct {
// (writes.go, issue 1945); CannotWriteSince since when. Said at once as a condition.
CannotWrite string `json:"cannot_write,omitempty"`
CannotWriteSince *time.Time `json:"cannot_write_since,omitempty"`
// NotHanded is why an approved merge that waits for nothing is not handed to the forge bridge (a member of its group that
// cannot be handed, and why), NotHandedSince since when: said once in the log and as a stalled line, never silent.
NotHanded string `json:"not_handed,omitempty"`
NotHandedSince *time.Time `json:"not_handed_since,omitempty"`
// UnactedSaid is the ask whose Approve was said not acted on, approvalBound after it (unacted.go): said once per ask.
UnactedSaid string `json:"unacted_said,omitempty"`
}
// declined is whether the operator's Decline stands for this run: it answered the ask the run holds now, or the run holds
// no ask any more. A Decline of an earlier ask, the run asked again since at other heads (a sibling's new head), does not
// stand (2026-10-29: hq#495 declined under one ask and approved under the next was never handed). A run kept before answers
// named their ask stands declined unless an Approve came after the Decline.
func (r *ReadyRun) declined() bool {
if r == nil || r.Declined == nil {
return false
}
if r.AnsweredAsk != "" {
return r.AskID == "" || r.AskID == r.AnsweredAsk
}
return r.Approved == nil || r.Declined.After(*r.Approved)
}
// MergeBranch is the one branch this owner merges into (novox/hq ADR 0398 §4): a merge into any other is its author's.
@@ -173,6 +197,9 @@ func (h *Holder) runOf(d *Delivery, now time.Time) *ReadyRun {
func (h *Holder) readyAsks(now time.Time) []func() {
var asks []func()
h.endStaleAsks()
// An Approve whose merge has not started is said, whatever holds it (unacted.go): read before this look hands, so a pass
// handed and given back within the last look (a read that failed) counts as not started.
h.sayUnacted(now)
gates := h.checkGates()
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
@@ -197,7 +224,7 @@ func (h *Holder) readyAsks(now time.Time) []func() {
asks = append(asks, a)
}
r := d.Ready
if r == nil || r.Merged != nil || r.Declined != nil {
if r == nil || r.Merged != nil || r.declined() {
continue
}
if d.State == Rejected {
@@ -276,9 +303,11 @@ func (h *Holder) readyAsks(now time.Time) []func() {
asks = append(asks, approvalAsk)
}
if waits != "" || r.merging {
h.noteNotHanded(d, "", now) // what it waits for is said by its waits, or it is handed
continue
}
if !r.LastTry.IsZero() {
h.noteNotHanded(d, "", now)
continue // refused: asked again at the next event that decides whether it may merge (mergeAgainNow), with no timer
}
// A group is merged one member at a time, handed to the forge bridge, and the pass stops at the first refusal (ADR
@@ -288,7 +317,10 @@ func (h *Holder) readyAsks(now time.Time) []func() {
g = nil
}
if a := h.handTurn(g, d, now); a != nil {
h.noteNotHanded(d, "", now)
asks = append(asks, a)
} else {
h.noteNotHanded(d, h.notHanded(g, d), now) // a merge never handed says why (2026-10-29: silent for an hour)
}
}
asks = append(asks, h.turnWrites()...) // the merge turns, after the holds of this look (ADR 0548 §3)
@@ -575,6 +607,11 @@ func (h *Holder) readyLines(now time.Time) []StalledLine {
Bound: "0s", H2: "none: a new head, which asks the operator again — the operator's",
Says: fmt.Sprintf("%s#%d at %s is not merged: the operator's answer %s", d.Repository,
d.Number, shortOf(r.Head, 12), r.Refused)})
case r.NotHanded != "" && r.NotHandedSince != nil && r.Approved != nil:
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
For: now.Sub(*r.NotHandedSince).Round(time.Second).String(), Bound: "0s",
H2: "none: the member named is approved at its head and ready, or its group is asked anew by a new head — the operator's",
Says: fmt.Sprintf("%s#%d at %s is approved and %s", d.Repository, d.Number, shortOf(r.Head, 12), r.NotHanded)})
case h.answering(d, now):
// The trunk moved and its check was asked again: the gate is answering it, so it is no stall yet (ADR 0505
// §4). `show` lists the refusal meanwhile.
@@ -110,7 +110,7 @@ func (h *Holder) approvedMerges() []approvedMerge {
for _, id := range ids {
d := h.deliveries[id]
r := d.Ready
if r == nil || r.Approved == nil || r.Declined != nil || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
if r == nil || r.Approved == nil || r.declined() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
d.State.Final() || d.State == Rejected || d.Base != MergeBranch || r.RefusedSince != nil || h.allMerged(r.Heads) {
continue
}
@@ -382,7 +382,10 @@ type TurnLine struct {
func (h *Holder) TurnsRead() map[string]any {
h.mu.Lock()
defer h.mu.Unlock()
want := h.wantedTurns()
want := map[string]sdk.Turn{}
if h.checkGates() {
want = h.wantedTurns() // as turnWrites: while the check gates no merge, no turn is written by design
}
repos := make([]string, 0, len(want))
for repo := range want {
repos = append(repos, repo)
@@ -404,6 +407,9 @@ func (h *Holder) TurnsRead() map[string]any {
lines = append(lines, l)
}
out := map[string]any{"turns": lines, "check-gates-merge": h.checkGates(), "bound": turnBound.String()}
if !h.checkGates() {
out["why-none"] = "the check gates no merge (check-gates-merge false): no merge waits for a check, so no turn is written"
}
if h.Turns != nil {
out["written"] = h.turnsLoaded
var unwritten []string
@@ -0,0 +1,167 @@
package main
import (
"fmt"
"sort"
"strings"
"time"
)
// **An Approve is acted on, or said at once** (the operator, 2026-10-30: "an approval given by me must get handled
// accordingly and you should know about it immediately"). That morning the group feat/774-l3-reland was approved at 10:14
// and nothing was handed for an hour, with no word: six of its seven members kept a Decline of an earlier ask beside the
// new Approve (the fix of 2026-10-29, reverted with L3, came back with this file), readyAsks skipped them and handTurn
// returned nil for the seventh.
//
// So, whatever holds it: every Approve is said on the bus as `mesh-delivery-gate.approval` {state: approved} when it is
// taken; an approved merge whose pass has not started approvalBound after its Approve is said again as {state: not-handed}
// with what it waits on, once per ask, and is an urgent stalled line (so an urgent condition) until its pass starts, unless
// a line of its own says it already (a held group; a merge waiting for another delivery's check, said at its bound). A
// rejected delivery is its author's, not an Approve unacted. A session waits on `mesh-delivery-gate.approval` to hear both.
// approvalBound is how long after the operator's Approve a merge's pass may take to start before it is said.
const approvalBound = 60 * time.Second
// approvalEvent is the event family of an Approve taken and of an Approve not acted on.
const approvalEvent = "approval"
// unacted is an approved ask whose pass has not started: the run holding its Approve, its owner, its members, and what
// the gate says it waits on.
type unacted struct {
owner *Delivery
run *ReadyRun
name string
members []string
waits string
// checkHeld is whether a member waits for the check of other heads (ADR 0548 §2): said by the event at once, and as a
// line by turnLines at its own bound, not twice.
checkHeld bool
}
// approvalStands is whether a run's Approve still stands: no Decline after it, not merged, into the merge branch.
func approvalStands(d *Delivery) bool {
r := d.Ready
if r == nil || r.Approved == nil || r.AskID == "" || len(r.Heads) == 0 || r.Merged != nil || d.MergedAs != "" ||
d.State.Final() || d.State == Rejected || d.Base != MergeBranch {
return false
}
return r.Declined == nil || !r.Declined.After(*r.Approved)
}
// unactedAsks are the approved asks whose pass has not started, each once, in the order of their ids. A pass has started
// when a member it covers is handed (merging), was tried (refused or asked again), or merged. Under the lock.
func (h *Holder) unactedAsks() []unacted {
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids)
seen := map[string]bool{}
var out []unacted
for _, id := range ids {
d := h.deliveries[id]
if !approvalStands(d) || seen[d.Ready.AskID] {
continue
}
if g := h.groups[d.Group]; d.Group != "" && g != nil && g.Hold != nil {
seen[d.Ready.AskID] = true
continue // a held group is said by its own urgent line, naming the member and why (groupLines)
}
r := d.Ready
seen[r.AskID] = true
u := unacted{owner: d, run: r, name: askGroup(d)}
started := false
var says []string
for mid := range r.Heads {
u.members = append(u.members, mid)
m := h.deliveries[mid]
if m == nil {
continue
}
if m.MergedAs != "" || (m.Ready != nil && (m.Ready.Merged != nil || m.Ready.merging || !m.Ready.LastTry.IsZero() ||
m.Ready.RefusedSince != nil)) {
started = true
break
}
if m.Ready != nil {
u.checkHeld = u.checkHeld || m.Ready.HeldSince != nil
if w := firstOf(m.Ready.NotHanded, m.Ready.Waits); w != "" {
says = append(says, mid+": "+w)
}
}
}
if started {
continue
}
sort.Strings(u.members)
sort.Strings(says)
u.waits = firstOf(strings.Join(says, "; "), "nothing the gate says: its merge was never handed")
out = append(out, u)
}
return out
}
// sayUnacted owes the event {state: not-handed} once per ask for every approved merge past approvalBound whose pass has
// not started, and wants a look at the bound of each not yet due. Under the lock; the event is owed, said by Flush.
func (h *Holder) sayUnacted(now time.Time) {
for _, u := range h.unactedAsks() {
due := u.run.Approved.Add(approvalBound)
if now.Before(due) {
h.lookAt(due, "an Approve whose merge has not started, said at its bound")
continue
}
if u.run.UnactedSaid == u.run.AskID {
continue
}
u.run.UnactedSaid = u.run.AskID
h.Logf("[mesh-delivery] %s is approved since %s and its merge is NOT handed: %s", u.name,
u.run.Approved.UTC().Format(time.RFC3339), u.waits)
u.owner.Owed = append(u.owner.Owed, Effect{Kind: EffectEmit, Event: approvalEvent, Since: now,
Line: string(mustJSON(approvalBody(u.owner, "not-handed", u.members, now, u.waits)))})
h.keep(u.owner)
}
}
// oweApproved owes the event {state: approved} for an ask the operator approved, on the first delivery it covers, and wants
// a look at its bound. Under the lock.
func (h *Holder) oweApproved(d *Delivery, now time.Time) {
r := d.Ready
members := make([]string, 0, len(r.Heads))
for id := range r.Heads {
members = append(members, id)
}
sort.Strings(members)
d.Owed = append(d.Owed, Effect{Kind: EffectEmit, Event: approvalEvent, Since: now,
Line: string(mustJSON(approvalBody(d, "approved", members, now, "")))})
h.lookAt(now.Add(approvalBound), "an Approve whose merge has not started, said at its bound")
}
// approvalBody is what `mesh-delivery-gate.approval` says: no secret, no address.
func approvalBody(d *Delivery, state string, members []string, now time.Time, waits string) map[string]any {
r := d.Ready
e := map[string]any{"state": state, "group": askGroup(d), "ask": r.AskID, "approved": r.Approved.UTC(), "at": now.UTC(),
"members": members, "by": r.Warrant}
if waits != "" {
e["waits"] = waits
e["for"] = now.Sub(*r.Approved).Round(time.Second).String()
}
return e
}
// unactedLines are the urgent stalled lines of approved merges whose pass has not started past approvalBound. Under the
// lock.
func (h *Holder) unactedLines(now time.Time) []StalledLine {
var out []StalledLine
for _, u := range h.unactedAsks() {
since := now.Sub(*u.run.Approved)
if since < approvalBound || u.checkHeld {
continue
}
out = append(out, StalledLine{ID: u.owner.ID, Number: u.owner.Number, State: u.owner.State,
For: since.Round(time.Second).String(), Bound: approvalBound.String(), Severity: "urgent",
H2: "none: the merge is handed once what it waits on ends; delivery_turns and mesh-delivery.show say what holds it",
Says: fmt.Sprintf("%s was approved at %s (%d pull requests) and its merge is not handed: %s", u.name,
u.run.Approved.UTC().Format(time.RFC3339), len(u.members), u.waits)})
}
return out
}
@@ -230,6 +230,7 @@ func (h *Holder) Stalled() []StalledLine {
out = append(out, h.readyLines(now)...)
out = append(out, h.turnLines(now)...) // an approved merge held past its bound by another delivery's check (ADR 0548 §4)
out = append(out, h.groupLines(now)...)
out = append(out, h.unactedLines(now)...) // an Approve whose merge has not started past its bound (unacted.go)
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
+2 -1
View File
@@ -33,7 +33,8 @@
"emits": [
"transition",
"group",
"merged"
"merged",
"approval"
],
"uses": [
"operator-channel"
@@ -161,7 +161,11 @@ func (m *memAsks) PutLinksAsked(key string, times []time.Time) error {
var rootFreeOf = map[*Holder]map[string]string{}
// told is what the router published to askers, by subject.
type told struct{ by map[string][]asks.Warrant }
type told struct {
by map[string][]asks.Warrant
// asked are the asks the router said it took, by subject (sayAsked).
asked map[string][]map[string]any
}
func (w *told) of(asker string) []asks.Warrant { return w.by[asks.DecidedSubject(asker)] }
@@ -171,7 +175,7 @@ func newRouter(t *testing.T) (*Holder, *fakeChannel, *fakeChannel, *clock, *memA
t.Helper()
h, tg, dt, c, _ := newHolder(t)
store := newMemAsks()
w := &told{by: map[string][]asks.Warrant{}}
w := &told{by: map[string][]asks.Warrant{}, asked: map[string][]map[string]any{}}
bench := NewBench(c.now)
bench.SetKinds([]KindInfo{
{Kind: "telegram", Module: "telegram", Node: "anchor", Capabilities: []string{"deliver", "reaches-away", "choice", "edit",
@@ -204,6 +208,9 @@ func newRouter(t *testing.T) (*Holder, *fakeChannel, *fakeChannel, *clock, *memA
if wt, ok := body.(asks.Warrant); ok {
w.by[subject] = append(w.by[subject], wt)
}
if m, ok := body.(map[string]any); ok && strings.HasPrefix(subject, "mesh.seat."+asks.Seat+".event."+AskedEvent+".") {
w.asked[subject] = append(w.asked[subject], m)
}
return nil
}
return h, tg, dt, c, store, w
@@ -325,6 +332,10 @@ func TestWithoutALinkedAccountOnlyTheMeshMCPServerApproves(t *testing.T) {
!strings.Contains(rec.HeldWhy, "no Telegram account is linked") {
t.Fatalf("not held: %+v %+v", rec, w.of("mesh-delivery"))
}
// Held or shown, the ask is said taken on the bus for its asker, so a session hears it open (2026-10-30).
if said := w.asked[askedSubject("mesh-delivery")]; len(said) != 1 || said[0]["ask"] != "d-1" || said[0]["state"] != StateHeld {
t.Fatalf("the ask taken is not said on %s: %+v", askedSubject("mesh-delivery"), said)
}
if len(dt.sends) != 0 {
t.Errorf("the desk was offered an ask with an approval: %+v", dt.sends)
}
+26
View File
@@ -560,6 +560,7 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
} else {
h.keepAsk(key, rec, 0)
}
h.sayAsked(asker, a, rec)
if rec.State == StateHeld {
h.Logf("[messenger] %s asks %q (%s): held, not shown: %s", asker, a.Headline, a.ID, rec.HeldWhy)
return nil
@@ -568,6 +569,31 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
return nil
}
// AskedEvent is the seat's event that an ask was taken, said for its asker as decided is (ADR 0259 §3): a session
// waits on `operator-channel.asked.<asker>` to hear an ask open, and on `operator-channel.decided.<asker>` to hear
// its answer (the operator, 2026-10-30: "you should be able to hook into approval requests so you also get a
// notification"). The SDK names decided's subject; asked's is named here until the SDK's asks contract carries it.
const AskedEvent = "asked"
// askedSubject is where the router says an ask of an asker was taken.
func askedSubject(asker string) string {
return "mesh.seat." + asks.Seat + ".event." + AskedEvent + "." + asker
}
// sayAsked says on the bus that an ask was taken, open or held: its id, asker, what it is about, its headline (the
// words the operator's phone shows, already judged to carry nothing that may not leave the mesh) and when it ends.
// Never its options' bindings or its whole words. A failure is said in the log and holds nothing: the ask stands.
func (h *Holder) sayAsked(asker string, a asks.Ask, rec AskRecord) {
if h.Publish == nil {
return
}
body := map[string]any{"ask": a.ID, "asker": asker, "about": a.About, "headline": a.Headline, "state": rec.State,
"opened": rec.Opened.UTC(), "expires": a.Expires}
if err := h.Publish(askedSubject(asker), body, "asked."+ticket()); err != nil {
h.Logf("[messenger] the ask %s of %s is taken and could not be said on the bus: %v", a.ID, asker, err)
}
}
// ticketsFor is every option's ticket on every kind that can carry the ask's highest level now: an ask with an
// approval is answered only where it could be approved (answerLevel). None when no ready channel can.
func (h *Holder) ticketsFor(a asks.Ask) map[string]Ticket {
@@ -64,8 +64,8 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
m, _ := readManifest(t)
verbs := []string{"open", "history", "notify", "tell", "asks"}
want := []seat{
{Name: asks.Seat, Scope: "mesh", Serves: verbs, Accepts: []string{"ask", "cancel", "condition"}, Emits: []string{"decided"},
ByCaller: []string{"ask", "cancel", "condition", "decided"}, Records: []string{"asks"}},
{Name: asks.Seat, Scope: "mesh", Serves: verbs, Accepts: []string{"ask", "cancel", "condition"}, Emits: []string{"decided", "asked"},
ByCaller: []string{"ask", "cancel", "condition", "decided", "asked"}, Records: []string{"asks"}},
{Name: asks.ChannelSeat, Scope: "mesh", Kinded: true, Accepts: []string{asks.Show, asks.Edit, asks.Send}},
{Name: asks.IntakeSeat, Scope: "mesh", Kinded: true,
Emits: []string{asks.Chosen, asks.Link, asks.FailedWhat, asks.StandingWhat}, Proofs: []string{asks.CodeProof}},
+4 -2
View File
@@ -20,13 +20,15 @@
"condition"
],
"emits": [
"decided"
"decided",
"asked"
],
"by-caller": [
"ask",
"cancel",
"condition",
"decided"
"decided",
"asked"
],
"records": [
"asks"