Files
mesh-delivery/modules/mesh-delivery-teller/cmd/mesh-delivery-teller-tools/main.go
T
jschoubben 0f4a33ce48 TL5: the teller carries the assigner's asks only, follows the view's answers and says its own on views.answers
The merge-ask path goes whole with the Approve per merge: the reads of
mesh-delivery-gate.asks and mesh-delivery-view.answers, OnAsk, OnAskGone, the
gate branches of Answer and the room list, and the merge words. The teller
follows the view's answers through view_read_answers and views.answers, says
its state answers as views.answers from its state teller-marks, and serves the
read tool teller_read_answers. The room list stays for the assigner's asks
(the lead's answer). viewstate gains the names the view's own commit gives it
(the same hunk, so the two merge cleanly).

Builds: design 47 G §653, §431, §512; lead answers A-172, A-175, A-178

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:52:42 +01:00

228 lines
9.6 KiB
Go

// Command mesh-delivery-teller-tools serves the teller's recovery tool (issue 774): the runtime launches it for a call.
// teller_end_ask writes one value of the teller's state ends and waits for the teller's answer there. The teller is the
// other program: it serves no request subject (ADR 0437 §4).
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-teller/internal/ends"
)
// widensSaidState is the teller's state widens-said (the program's own constant of it is in cmd/mesh-delivery-teller).
const widensSaidState = "widens-said"
// asker writes an ends value and waits for the teller's answer to it.
type asker func(key string, value []byte) (ends.End, error)
// Tools are the teller's tools.
func Tools(ask asker, now func() time.Time) []stdio.Tool {
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
return []stdio.Tool{
{Name: ends.Tool,
Description: "End one open ask the delivery teller holds at the operator channel, by hand, saying why: the teller " +
"cancels it at the router, which frees one of the three places the router gives the teller, or drops it from the " +
"teller's wait for room. For an ask whose question is gone but whose key was never taken away (a merge ask of a pull " +
"request merged by another path, a backup ask about a backup no longer failing), which otherwise holds its place " +
"until it lapses, so a newer ask is refused for want of room. Name the ask by its router id (as " +
"operator-channel's asks show it) or by the key of the gate's or the assigner's asks. Answers which ask it found, " +
"whether it ended it, and what it did; an ask answered, lapsed or ended already is left and said so.",
Input: map[string]any{"type": "object", "required": []string{"ask", "why"}, "properties": map[string]any{
"ask": map[string]any{"type": "string", "description": "the ask's router id, or the key of the gate's or the assigner's asks it was asked for"},
"why": map[string]any{"type": "string", "description": "why it is ended by hand, in plain words"}}},
Run: func(args map[string]any) (any, error) {
key, value, err := ends.New(str(args, "ask"), str(args, "why"), stdio.CallerOf(args), now())
if err != nil {
return nil, err
}
e, err := ask(key, value)
if err != nil {
return nil, err
}
return e.Result, nil
}},
}
}
// AnswersTool is the tool that reads the teller's state answers (ADR 0596 §3: the owner's state is read through its owner's
// tool): one answer by the key of the ask it answers, or every answer when none is named. It reads and writes nothing else.
const AnswersTool = "teller_answers"
// answers is the teller's state answers as the tool reads it.
type answers interface {
Get(key string) (*stdio.StateEntry, error)
Keys() ([]string, error)
}
// AnswerTools is the tool teller_answers over the state answers.
func AnswerTools(s answers) []stdio.Tool {
return []stdio.Tool{{Name: AnswersTool,
Description: "Read the answers the delivery teller kept to the gate's asks, as it wrote them: the answer under one ask's " +
"key (found false when the ask has none), or every answer when no key is named. An answer is the operator's chosen " +
"option, the warrant it came by and the words of it. What the gate reads to merge on the Approve.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"key": map[string]any{"type": "string", "description": "the key of the gate's ask the answer is under; none for every answer"}}},
Run: func(args map[string]any) (any, error) {
key, _ := args["key"].(string)
if key = strings.TrimSpace(key); key != "" {
e, err := s.Get(key)
if err != nil {
return nil, fmt.Errorf("the answer under %s cannot be read: %w", key, err)
}
if e == nil {
return map[string]any{"key": key, "found": false}, nil
}
return map[string]any{"key": key, "found": true, "answer": json.RawMessage(e.Value)}, nil
}
keys, err := s.Keys()
if err != nil {
return nil, fmt.Errorf("the answers cannot be listed: %w", err)
}
sort.Strings(keys)
out := make([]map[string]any, 0, len(keys))
for _, k := range keys {
e, err := s.Get(k)
if err != nil {
return nil, fmt.Errorf("the answer under %s cannot be read: %w", k, err)
}
if e != nil {
out = append(out, map[string]any{"key": k, "answer": json.RawMessage(e.Value)})
}
}
return map[string]any{"answers": out}, nil
}}}
}
// ReadAnswersTool is the teller's ordinary read tool of its state answers (design 47 G A-175, A-178; mesh-sdk views.ReadTool):
// what the view follows, first by it and then by the teller's views family views.answers. Its marks are the state teller-marks.
const ReadAnswersTool = "teller_read_answers"
// marksState is the teller's state of what its views family said each state up to (A-178).
const marksState = "teller-marks"
// ReadAnswersTools is the tool teller_read_answers over the state answers and its marks.
func ReadAnswersTools(values, marks views.Source, now func() time.Time) []stdio.Tool {
return []stdio.Tool{views.ReadTool("mesh-delivery-teller", ReadAnswersTool, "answers",
"the operator's answers to the assigner's asks, by the ask's key.", values, marks, now)}
}
// WidensSaidTool reads the teller's state widens-said (design 47 G §335).
const WidensSaidTool = "teller_widens_said"
// WidensSaidTools is the tool teller_widens_said over the state widens-said: the last commit the teller said a widening of
// for one module on one watched branch, or every one when none is named.
func WidensSaidTools(s answers) []stdio.Tool {
return []stdio.Tool{{Name: WidensSaidTool,
Description: "Read what the delivery teller has said of widenings: for each module and watched branch, the last commit it told the " +
"operator a widening of on every channel. Keys are the module and the branch, each escaped as a key's name is, joined by a dot. " +
"Name a key for one, or none for every one.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"key": map[string]any{"type": "string", "description": "<module>.<branch> as the state keys it; none for every one"}}},
Run: func(args map[string]any) (any, error) {
key, _ := args["key"].(string)
if key = strings.TrimSpace(key); key != "" {
e, err := s.Get(key)
if err != nil {
return nil, fmt.Errorf("%s cannot be read: %w", key, err)
}
if e == nil {
return map[string]any{"key": key, "found": false}, nil
}
return map[string]any{"key": key, "found": true, "commit": json.RawMessage(e.Value)}, nil
}
keys, err := s.Keys()
if err != nil {
return nil, fmt.Errorf("the keys cannot be listed: %w", err)
}
sort.Strings(keys)
out := make([]map[string]any, 0, len(keys))
for _, k := range keys {
e, err := s.Get(k)
if err != nil {
return nil, fmt.Errorf("%s cannot be read: %w", k, err)
}
if e != nil {
out = append(out, map[string]any{"key": k, "commit": json.RawMessage(e.Value)})
}
}
return map[string]any{"said": out}, nil
}}}
}
// askEnd writes the ends value at key and waits, on a watch of the key, at most bound for the teller's answer.
func askEnd(s stdio.KeptState, bound time.Duration) asker {
return func(key string, value []byte) (ends.End, error) {
answered := make(chan ends.End, 1)
var rev uint64
var mu sync.Mutex
take := func(revision uint64, v []byte) {
mu.Lock()
mine := rev
mu.Unlock()
if mine == 0 || revision <= mine {
return
}
e, err := ends.Decode(v)
if err != nil || !e.Done() {
return
}
select {
case answered <- e:
default:
}
}
if err := s.Watch(key, func(c stdio.StateChange) error {
if c.Op == "put" {
take(c.Revision, c.Value)
}
return nil
}); err != nil {
return ends.End{}, fmt.Errorf("watching %s.%s: %w", ends.State, key, err)
}
r, err := s.Put(key, json.RawMessage(value))
if err != nil {
return ends.End{}, fmt.Errorf("the end could not be written to %s: %w", ends.Bucket, err)
}
mu.Lock()
rev = r
mu.Unlock()
// The watch may have heard the answer before the revision was known: read it once too.
if e, err := s.Get(key); err == nil && e != nil {
take(e.Revision, e.Value)
}
wait, cancel := context.WithTimeout(context.Background(), bound) // a context's bound, no timer (ADR 0437 §8)
defer cancel()
select {
case e := <-answered:
return e, nil
case <-wait.Done():
return ends.End{}, fmt.Errorf("the teller did not answer within %s: is it running? its answer will be in %s.%s", bound, ends.Bucket, key)
}
}
}
func main() {
// teller_first_views and teller_first_view_again: where the teller's first view of the assigner's asks stands, and
// asking it again by hand (ADR 0615, design 25 § 12.2).
tools := append(Tools(askEnd(stdio.State(ends.State), 25*time.Second), time.Now),
views.StatusTools("mesh-delivery-teller", "teller_first_views", "teller_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
tools = append(tools, AnswerTools(stdio.State("answers"))...)
tools = append(tools, ReadAnswersTools(views.KeptSource{State: stdio.State("answers")}, views.KeptSource{State: stdio.State(marksState)}, time.Now)...)
tools = append(tools, WidensSaidTools(stdio.State(widensSaidState))...)
if err := stdio.Serve("", tools); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-teller-tools] %v\n", err)
os.Exit(1)
}
}