Files
mesh-delivery/modules/mesh-delivery-watchman/internal/watch/dead.go
T
jschoubben 43d4f511c5 Drop the lease write's findings, the controller's artifacts-gone and the issuing gate (issue 1126, B9)
ADR 0590 supersedes the lease write: mesh-grants says no lease event any
more, so the watchman's lease-altered and removal-failed findings and
their row had nothing to hear. The ended controller's consumers are
removed as hand acts, so their deletion is no consumer lost; only the
one on DEAD_LETTER_NOTICES is still read, since its removal hands the
notices to the watchman. The builder hears artifacts-gone from the
artifact sweep seat alone, and mesh-enrolment's tokens no longer report
an issuing gate that nothing can turn off.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:35:55 +01:00

92 lines
3.7 KiB
Go

package watch
import (
"fmt"
"sort"
"strings"
"time"
)
// The dead letters' findings (novox/hq ADR 0564 §5, issue 330): a consumer DEAD_LETTERS holds a message for is said for as
// long as it holds any, `bus.<stream>.<consumer>.max-deliveries`, and the notices waiting behind the ended controller's
// consumer are said as `bus.DEAD_LETTER_NOTICES.controller.notices-waiting`.
// DeadRecountEvery is how often DEAD_LETTERS is counted again while a consumer's finding stands: a letter delivered again
// or dropped by the controller's verb, which the watchman does not hear, ends its finding within it. The expiry of the
// watchman's own pulse, never a loop.
const DeadRecountEvery = 5 * time.Minute
// KindNoticesWaiting is the finding of notices waiting while the ended controller's consumer stands.
const KindNoticesWaiting = "notices-waiting"
// DeadLettersHeld is DEAD_LETTERS' count per consumer, `<stream>.<consumer>`, read whole: each consumer held writes its
// finding, each one no longer held has it deleted. who words a consumer.
func (w *Watchman) DeadLettersHeld(held map[string]int, who func(stream, consumer string) string) {
w.mu.Lock()
defer w.mu.Unlock()
want := map[string]bool{}
keys := make([]string, 0, len(held))
for k := range held {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
n := held[key]
stream, consumer, ok := strings.Cut(key, ".")
if !ok || n <= 0 {
continue
}
messages, them := "a message", "it is"
if n > 1 {
messages, them = fmt.Sprintf("%d messages", n), "they are"
}
f := Finding{Scope: ScopeBus, ID: key, Kind: KindDeadLetters, Severity: Warning, Source: "S9",
Machine: consumerMachine(stream, consumer),
Summary: fmt.Sprintf("%s gave up on %s; %s kept in DEAD_LETTERS until delivered again or dropped, with why",
who(stream, consumer), messages, them),
Said: fmt.Sprintf("%d held for %s (counted again every %s while any is held, so one delivered again or dropped "+
"ends this within it)", n, key, DeadRecountEvery),
Needs: "deliver them again or drop them, from the mesh MCP server."}
want[f.Key()] = true
w.find(f)
}
for key := range w.findings {
if strings.HasPrefix(key, ScopeBus+".") && strings.HasSuffix(key, "."+KindDeadLetters) && !want[key] {
w.unfind(key)
}
}
if len(want) == 0 {
w.unsignal("dead", "recount")
return
}
w.signal("dead", "recount", DeadRecountEvery, signalDue{Do: dueRecount,
Finding: Finding{Scope: ScopeBus, ID: "DEAD_LETTERS", Kind: KindDeadLetters, Severity: Warning, Summary: "counted again", Source: "S9"}})
}
// consumerMachine is the machine a module's consumer on EVENTS is on; empty for the others.
func consumerMachine(stream, consumer string) string {
if stream == "EVENTS" {
if node, _, ok := strings.Cut(consumer, "_"); ok {
return node
}
}
return ""
}
// NoticesWaiting says whether the ended controller's consumer on DEAD_LETTER_NOTICES still stands: every notice then waits
// in the queue, kept a week, until that consumer is removed (novox/hq ADR 0590 clause 6).
func (w *Watchman) NoticesWaiting(waiting bool) {
w.mu.Lock()
defer w.mu.Unlock()
f := Finding{Scope: ScopeBus, ID: "DEAD_LETTER_NOTICES.controller", Kind: KindNoticesWaiting, Severity: Warning, Source: "dead-letters",
Summary: "the controller is unassigned but its consumer on DEAD_LETTER_NOTICES still stands, so every notice of a " +
"message given up on waits in the queue, kept a week, and nothing keeps it in DEAD_LETTERS",
Said: "the consumer controller on DEAD_LETTER_NOTICES stands after the controller's end",
Needs: "remove the consumer controller on DEAD_LETTER_NOTICES with the bus module's tool, recorded as a hand act (ADR 0590)."}
if waiting {
w.find(f)
return
}
w.unfind(f.Key())
}