Compare commits

..
35 changed files with 113 additions and 490 deletions
@@ -47,6 +47,9 @@ func (m *Module) askSteps(steps []StepTool) buildstep.AskSteps {
Has: m.Has,
Release: func(ws string) error { return m.spaces().Release(ws) },
Logf: m.Say,
// A failed test's whole output is said on the module's own log stream (buildstep.LogTool reads it back).
Narrate: m.Emit,
Module: "mesh-build-go",
}
}
@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"os"
"path/filepath"
"slices"
@@ -47,9 +48,14 @@ func TestTheManifestDeclaresTheToolchainsStream(t *testing.T) {
Streams []struct {
Name string `json:"name"`
Events []string `json:"events"`
Age int `json:"max-age-days"`
MiB int `json:"max-mib"`
} `json:"streams"`
}
if err := json.Unmarshal(raw, &m); err != nil || len(m.Streams) != 1 || m.Streams[0].Name != "toolchains-go" || !slices.Equal(m.Streams[0].Events, []string{"toolchain.**"}) {
// And its log stream: a failed test's whole output, kept 14 days and at most 2 GiB, dropping its oldest (the operator,
// 2026-10-30).
if err := json.Unmarshal(raw, &m); err != nil || len(m.Streams) != 2 || m.Streams[0].Name != "toolchains-go" || !slices.Equal(m.Streams[0].Events, []string{"toolchain.**"}) ||
m.Streams[1].Name != buildstep.LogStreamOf("go") || !slices.Equal(m.Streams[1].Events, []string{"log.**"}) || m.Streams[1].Age != 14 || m.Streams[1].MiB != 2048 {
t.Errorf("streams = %+v (%v)", m.Streams, err)
}
}
@@ -14,6 +14,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -41,6 +42,7 @@ func main() {
Emit: func(t string, b any) error { return stdio.Emit(t, b) },
Steps: stdio.State(delivery.StepsState).Put,
ReadSteps: readOf(stdio.State(delivery.StepsState)),
ReadLog: stdio.Stream(buildstep.LogStreamOf("go")).Read,
Pauses: stdio.State(delivery.PausesState).Put,
ReadPauses: readOf(stdio.State(delivery.PausesState)),
ReadToolchainsState: readOf(stdio.State(delivery.ToolchainsState)),
@@ -31,6 +31,9 @@ type Module struct {
// Held is the puller whose running asks a step-end tool will not end (issue 1890); nil in a test.
Held *buildstep.Puller
ReadSteps, ReadToolchainsState StateRead
// ReadLog reads the module's own log stream (stdio.Stream(buildstep.LogStreamOf(language)).Read), where each step
// says a failed test's whole output (Emit says it); nil in a test.
ReadLog buildstep.StreamRead
// Pauses is the Put of the pauses state, which keeps the node's pause key without an age; ReadPauses reads it back.
Pauses buildstep.Put
ReadPauses StateRead
@@ -115,7 +115,7 @@ func TestAGoThatCannotBeRunRefusesEveryStepAndHealthIsUnhealthy(t *testing.T) {
{Name: "short_probe", Run: run},
}, m.StepTools()...)
for _, tool := range m.Tools(steps) {
if tool.Name == StatusTool || tool.Name == HealthTool || tool.Name == StepReadTool || tool.Name == StepsReadTool || tool.Name == ToolchainsReadTool || tool.Name == ReleaseTool || tool.Name == StepEndTool {
if tool.Name == StatusTool || tool.Name == HealthTool || tool.Name == StepReadTool || tool.Name == StepsReadTool || tool.Name == LogReadTool || tool.Name == ToolchainsReadTool || tool.Name == ReleaseTool || tool.Name == StepEndTool {
continue
}
var reason string
@@ -15,13 +15,16 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The read tools, named as the module is: build_go_ and the verb.
const (
StepReadTool = "build_go_step"
StepsReadTool = "build_go_steps"
StepReadTool = "build_go_step"
StepsReadTool = "build_go_steps"
// LogReadTool reads one step's log, a failed test's whole output, from the module's own log stream.
LogReadTool = "build_go_log"
ToolchainsReadTool = "build_go_toolchains"
)
@@ -225,7 +228,14 @@ func (m *Module) ReadToolchains() (map[string]delivery.Toolchain, error) {
// ReadTools are the module's read tools. All are read-only.
func (m *Module) ReadTools() []stdio.Tool {
readLog := m.ReadLog
if readLog == nil {
readLog = func(string, uint64, int) (*stdio.StreamPage, error) {
return nil, fmt.Errorf("this process reads no log stream: it was not launched by the runtime")
}
}
return []stdio.Tool{
buildstep.LogTool(LogReadTool, readLog),
{Name: StepReadTool,
Description: "What one step of this machine ended with, as stored in the module's steps state: its tool, state " +
"(running, passed, failed, refused), when it started and ended, why it failed or was refused, and its result " +
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildgo
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+6
View File
@@ -1,5 +1,11 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095131-16bc0dcf3e9d h1:jFOz6wUfZvIayd4DctwlaFKKZHQNME6NLs2OS1rNv30=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095131-16bc0dcf3e9d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc h1:Wdjl2gomooHswJ5qpkwZPq/ureZgXVw6FMCsgzPJhPo=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde h1:eKXSllHCS31GgMazfvZ7BeiLoXIUibnUmzxWVkj+Vcw=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
+7
View File
@@ -51,6 +51,7 @@
"build_go_release",
"build_go_step",
"build_go_steps",
"build_go_log",
"build_go_step_end",
"build_go_toolchains",
"build_go_status",
@@ -60,6 +61,12 @@
{
"name": "toolchains-go",
"events": ["toolchain.**"]
},
{
"name": "go-build-logs",
"events": ["log.**"],
"max-age-days": 14,
"max-mib": 2048
}
],
"requires": [
+1
View File
@@ -0,0 +1 @@
/mesh-build-node
@@ -37,6 +37,9 @@ func (m *Module) askSteps(steps []StepTool) buildstep.AskSteps {
},
Release: func(ws string) error { return m.spaces().Release(ws) },
Logf: m.Say,
// A failed test's whole output is said on the module's own log stream (buildstep.LogTool reads it back).
Narrate: m.Emit,
Module: "mesh-build-node",
}
}
@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"os"
"path/filepath"
"slices"
@@ -47,9 +48,14 @@ func TestTheManifestDeclaresTheToolchainsStream(t *testing.T) {
Streams []struct {
Name string `json:"name"`
Events []string `json:"events"`
Age int `json:"max-age-days"`
MiB int `json:"max-mib"`
} `json:"streams"`
}
if err := json.Unmarshal(raw, &m); err != nil || len(m.Streams) != 1 || m.Streams[0].Name != "toolchains-node" || !slices.Equal(m.Streams[0].Events, []string{"toolchain.**"}) {
// And its log stream: a failed test's whole output, kept 14 days and at most 2 GiB, dropping its oldest (the operator,
// 2026-10-30).
if err := json.Unmarshal(raw, &m); err != nil || len(m.Streams) != 2 || m.Streams[0].Name != "toolchains-node" || !slices.Equal(m.Streams[0].Events, []string{"toolchain.**"}) ||
m.Streams[1].Name != buildstep.LogStreamOf("node") || !slices.Equal(m.Streams[1].Events, []string{"log.**"}) || m.Streams[1].Age != 14 || m.Streams[1].MiB != 2048 {
t.Errorf("streams = %+v (%v)", m.Streams, err)
}
}
@@ -16,6 +16,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
@@ -47,6 +48,7 @@ func main() {
Emit: func(t string, b any) error { return stdio.Emit(t, b) },
Steps: stdio.State(delivery.StepsState).Put,
ReadSteps: readOf(stdio.State(delivery.StepsState)),
ReadLog: stdio.Stream(buildstep.LogStreamOf("node")).Read,
Pauses: stdio.State(delivery.PausesState).Put,
ReadPauses: readOf(stdio.State(delivery.PausesState)),
ReadToolchainsState: readOf(stdio.State(delivery.ToolchainsState)),
@@ -45,6 +45,9 @@ type Module struct {
// Held is the puller whose running asks a step-end tool will not end (issue 1890); nil in a test.
Held *buildstep.Puller
ReadSteps, ReadToolchainsState StateRead
// ReadLog reads the module's own log stream (stdio.Stream(buildstep.LogStreamOf(language)).Read), where each step
// says a failed test's whole output (Emit says it); nil in a test.
ReadLog buildstep.StreamRead
// Pauses is the Put of the pauses state, which keeps the node's pause key without an age; ReadPauses reads it back.
Pauses buildstep.Put
ReadPauses StateRead
@@ -117,7 +117,7 @@ func TestANodeThatCannotBeRunRefusesEveryStepAndHealthIsUnhealthy(t *testing.T)
{Name: "short_probe", Run: run},
}, (&Module{}).StepTools()...)
for _, tool := range m.Tools(steps) {
if tool.Name == StatusTool || tool.Name == HealthTool || tool.Name == StepReadTool || tool.Name == StepsReadTool || tool.Name == ToolchainsReadTool || tool.Name == ReleaseTool || tool.Name == StepEndTool {
if tool.Name == StatusTool || tool.Name == HealthTool || tool.Name == StepReadTool || tool.Name == StepsReadTool || tool.Name == LogReadTool || tool.Name == ToolchainsReadTool || tool.Name == ReleaseTool || tool.Name == StepEndTool {
continue
}
var reason string
@@ -15,13 +15,16 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The read tools, named as the module is: build_node_ and the verb.
const (
StepReadTool = "build_node_step"
StepsReadTool = "build_node_steps"
StepReadTool = "build_node_step"
StepsReadTool = "build_node_steps"
// LogReadTool reads one step's log, a failed test's whole output, from the module's own log stream.
LogReadTool = "build_node_log"
ToolchainsReadTool = "build_node_toolchains"
)
@@ -225,7 +228,14 @@ func (m *Module) ReadToolchains() (map[string]delivery.Toolchain, error) {
// ReadTools are the module's read tools. All are read-only.
func (m *Module) ReadTools() []stdio.Tool {
readLog := m.ReadLog
if readLog == nil {
readLog = func(string, uint64, int) (*stdio.StreamPage, error) {
return nil, fmt.Errorf("this process reads no log stream: it was not launched by the runtime")
}
}
return []stdio.Tool{
buildstep.LogTool(LogReadTool, readLog),
{Name: StepReadTool,
Description: "What one step of this machine ended with, as stored in the module's steps state: its tool, state " +
"(running, passed, failed, refused), when it started and ended, why it failed or was refused, and its result " +
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildnode
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats.go v1.54.0
)
+6
View File
@@ -1,5 +1,11 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095131-16bc0dcf3e9d h1:jFOz6wUfZvIayd4DctwlaFKKZHQNME6NLs2OS1rNv30=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095131-16bc0dcf3e9d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc h1:Wdjl2gomooHswJ5qpkwZPq/ureZgXVw6FMCsgzPJhPo=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde h1:eKXSllHCS31GgMazfvZ7BeiLoXIUibnUmzxWVkj+Vcw=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
+7
View File
@@ -46,6 +46,7 @@
"build_node_release",
"build_node_step",
"build_node_steps",
"build_node_log",
"build_node_step_end",
"build_node_toolchains",
"build_node_status",
@@ -57,6 +58,12 @@
"events": [
"toolchain.**"
]
},
{
"name": "node-build-logs",
"events": ["log.**"],
"max-age-days": 14,
"max-mib": 2048
}
],
"requires": [
@@ -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() || (r.AskID == "" && r.Approved == nil) || len(r.Heads) == 0 {
if r == nil || r.Merged != nil || d.MergedAs != "" || r.Declined != nil || (r.AskID == "" && r.Approved == nil) || len(r.Heads) == 0 {
continue
}
switch {
@@ -418,7 +418,6 @@ 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))
@@ -609,7 +608,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() && !seen[r.AskID] {
if r := d.Ready; r != nil && r.AskID != "" && r.Approved == nil && r.Declined == nil && !seen[r.AskID] {
seen[r.AskID] = true
out = append(out, r.AskID)
}
@@ -706,19 +705,16 @@ 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, r.Declined, r.AnsweredAsk = &now, nil, key
r.Approved = &now
h.Logf("[mesh-delivery] %s: the operator approved its merge into main (%s)", d.ID, r.Warrant)
case OptionDecline:
r.Declined, r.AnsweredAsk = &now, key
r.Declined = &now
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,7 +1,6 @@
package main
import (
"fmt"
"strings"
"testing"
"time"
@@ -406,140 +405,3 @@ 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() || done[r.AskID] {
if !strings.EqualFold(d.Repository, repository) || r == nil || r.AskID == "" || r.Approved != nil || r.Declined != nil || 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() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
if r == nil || r.Approved == nil || r.Declined != nil || 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() || c.Heads[d.ID] != d.Commit {
if c == nil || c.Approved == nil || c.Declined != nil || 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() {
if r := h.covering(d); r != nil && r.AskID != "" && r.Approved == nil && r.Declined == nil {
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", "approval"}) {
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged"}) {
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,10 +149,11 @@ func (h *Holder) handTurn(g *Group, d *Delivery, now time.Time) func() {
return nil
}
for _, m := range order {
if h.notHandable(m) != "" {
return nil // the others are not approved at their heads yet: nothing is merged before they are (said by notHanded)
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
}
reads = append(reads, readOf(m, h.covering(m).Heads[m.ID]))
reads = append(reads, readOf(m, mc.Heads[m.ID]))
}
}
if c == nil || c.Approved == nil || c.Heads[d.ID] == "" {
@@ -168,66 +169,6 @@ 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,13 +120,11 @@ 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). 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"`
// 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"`
// 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
@@ -139,28 +137,6 @@ 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.
@@ -197,9 +173,6 @@ 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 {
@@ -224,7 +197,7 @@ func (h *Holder) readyAsks(now time.Time) []func() {
asks = append(asks, a)
}
r := d.Ready
if r == nil || r.Merged != nil || r.declined() {
if r == nil || r.Merged != nil || r.Declined != nil {
continue
}
if d.State == Rejected {
@@ -303,11 +276,9 @@ 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
@@ -317,10 +288,7 @@ 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)
@@ -607,11 +575,6 @@ 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() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
if r == nil || r.Approved == nil || r.Declined != nil || 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,10 +382,7 @@ type TurnLine struct {
func (h *Holder) TurnsRead() map[string]any {
h.mu.Lock()
defer h.mu.Unlock()
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
}
want := h.wantedTurns()
repos := make([]string, 0, len(want))
for repo := range want {
repos = append(repos, repo)
@@ -407,9 +404,6 @@ 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
@@ -1,167 +0,0 @@
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,7 +230,6 @@ 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
}
+1 -2
View File
@@ -33,8 +33,7 @@
"emits": [
"transition",
"group",
"merged",
"approval"
"merged"
],
"uses": [
"operator-channel"
@@ -161,11 +161,7 @@ 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
// asked are the asks the router said it took, by subject (sayAsked).
asked map[string][]map[string]any
}
type told struct{ by map[string][]asks.Warrant }
func (w *told) of(asker string) []asks.Warrant { return w.by[asks.DecidedSubject(asker)] }
@@ -175,7 +171,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{}, asked: map[string][]map[string]any{}}
w := &told{by: map[string][]asks.Warrant{}}
bench := NewBench(c.now)
bench.SetKinds([]KindInfo{
{Kind: "telegram", Module: "telegram", Node: "anchor", Capabilities: []string{"deliver", "reaches-away", "choice", "edit",
@@ -208,9 +204,6 @@ 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
@@ -332,10 +325,6 @@ 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,7 +560,6 @@ 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
@@ -569,31 +568,6 @@ 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", "asked"},
ByCaller: []string{"ask", "cancel", "condition", "decided", "asked"}, Records: []string{"asks"}},
{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.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}},
+2 -4
View File
@@ -20,15 +20,13 @@
"condition"
],
"emits": [
"decided",
"asked"
"decided"
],
"by-caller": [
"ask",
"cancel",
"condition",
"decided",
"asked"
"decided"
],
"records": [
"asks"