messenger hears and asks the condition keeper alone, never the controller (ADR 0564 clause 9)
The three mesh-controller condition-event consumes, the invoke of mesh-controller.conditions, the follow switch (takeover.go) and its read of mesh-conditions.takeover are gone: the messenger consumes mesh-conditions' events and reads its open conditions from seat mesh-conditions.conditions at its start. An ask of the keeper is shown on a condition's message like the controller's was. The manifest test now refuses any controller entry; the event test refuses the controller's events; the words fixture's action verb names the keeper's. No test added. (issue 774, 774/S12) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -10,8 +10,8 @@ channel's client and no channel's secret.
|
||||
a record each asker reads under its own name), and the two kinded benches its channels hold, `channel`
|
||||
(`show`, `edit`, `send`, each under the holder's kind) and `intake` (`choice`, `link`, `failed`, `standing`,
|
||||
and the proof `code`, request and reply only). Only `channel` and `intake` may be kinded.
|
||||
- **Reads the controller's open conditions** (`seat:mesh-controller.conditions`) and **consumes its
|
||||
condition events**; the events are read in one place, `cmd/messenger/condition.go`.
|
||||
- **Reads the condition keeper's open conditions** (`seat:mesh-conditions.conditions`) and **consumes its
|
||||
condition events** (the controller's are no longer heard, issue 774); the events are read in one place, `cmd/messenger/condition.go`.
|
||||
- **The channels are modules of their own**: `telegram` (kind `telegram`, reaches the operator away, proves
|
||||
its sender) and `desk-channel` (kind `desktop`, the machines' notifiers, proves nothing). Which kinds exist
|
||||
and what each promises is the controller's word, in this module's membership; whether each can send now
|
||||
@@ -94,7 +94,7 @@ handed over three hours of raised-and-cleared conditions at once, and each was s
|
||||
what is said is decided by the state now and by when a thing happened, never by when it arrived:
|
||||
|
||||
- **The state first.** On start, every 10 min, and at once when an old event arrives, the holder reads
|
||||
what is open now from the controller (`mesh-controller.conditions`). An event older than that
|
||||
what is open now from the condition keeper (`mesh-conditions.conditions`). An event older than that
|
||||
reading is already in it and is not acted on. What is open here and not there ended meanwhile: it is
|
||||
forgotten quietly (Telegram's message is edited, which notifies nobody; the desktop is not woken). An
|
||||
urgent condition open and never said is said — all of them in **one** message. A warning raised in
|
||||
|
||||
@@ -19,7 +19,7 @@ func (b *conditionBed) controllerEvent(t *testing.T, event string, body any) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.r.request("mesh/event", map[string]any{"envelope": map[string]any{"key": ControllerSeat + "." + event,
|
||||
if _, err := b.r.request("mesh/event", map[string]any{"envelope": map[string]any{"key": ConditionSeat + "." + event,
|
||||
"node": "test", "body": json.RawMessage(raw)}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -479,8 +479,8 @@ func answerLevel(a asks.Ask) asks.Level {
|
||||
return asks.Acknowledge
|
||||
}
|
||||
|
||||
// ConditionOwner is the module whose asks may be shown on a condition's message: the controller, which
|
||||
// raises the conditions.
|
||||
// ConditionOwner is the module whose asks may be shown on a condition's message: the controller, which raised the
|
||||
// conditions; the condition keeper (ConditionSeat), which raises them now, is one too.
|
||||
const ConditionOwner = "mesh-controller"
|
||||
|
||||
// TakeAsk takes one ask or cancel from the seat's queue. asker is the subject's last token, which the bus
|
||||
@@ -651,7 +651,7 @@ func (h *Holder) recordFor(asker, key string, a asks.Ask) string {
|
||||
defer h.mu.Unlock()
|
||||
// Shown on a condition's message only when asked by the condition's owner: another asker naming a
|
||||
// condition's key does not take its message over.
|
||||
if a.About != "" && asker == ConditionOwner {
|
||||
if a.About != "" && (asker == ConditionOwner || asker == ConditionSeat) {
|
||||
if r := h.open[a.About]; r != nil && r.Cleared.IsZero() && r.Origin == "condition" {
|
||||
return r.Key
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package main
|
||||
|
||||
// The controller's condition events, read in this one place (novox/hq to-be 45 §2).
|
||||
// The condition keeper's condition events, read in this one place (novox/hq to-be 45 §2, ADR 0564 clause 9).
|
||||
//
|
||||
// **The contract this reads, and every assumption it makes about it**, because the controller's
|
||||
// **The contract this reads, and every assumption it makes about it**, because the keeper's
|
||||
// side was built at the same time from the same design and the design names the events and the
|
||||
// fields but not their spelling on the wire:
|
||||
// fields but not their spelling on the wire (the controller's own events are no longer heard, issue 774):
|
||||
//
|
||||
// - The events are the mesh-controller seat's own: `condition-raised`, `condition-changed` and
|
||||
// `condition-cleared`, consumed as `mesh-controller.<event>` (subject
|
||||
// `mesh.seat.mesh-controller.event.<event>`), exactly as `applied` and `built-before` are.
|
||||
// - The events are the seat mesh-conditions': `condition-raised`, `condition-changed` and
|
||||
// `condition-cleared`, consumed as `mesh-conditions.<event>` (subject
|
||||
// `mesh.seat.mesh-conditions.event.<event>`).
|
||||
// - The body is the condition as the store holds it, one JSON object, with the field names of the
|
||||
// design's table in kebab-case: key, kind, subject, severity, summary, evidence, source, raised,
|
||||
// last-observed, observations, tried, resolver, silenced, epoch. snake_case and camelCase
|
||||
@@ -27,7 +27,7 @@ package main
|
||||
// - Every event carries `at`, when the transition happened (the controller's conditions.Event).
|
||||
// It is how this holder tells news from history: a stream replays what it holds to a consumer
|
||||
// that is new or was away, and an event's delivery time says nothing about when it happened.
|
||||
// - The `conditions` verb of the controller's seat answers `{conditions: [...], open, note}`, each
|
||||
// - The `conditions` verb of the keeper's seat answers `{conditions: [...], open, note}`, each
|
||||
// entry the condition as the store holds it — the same shape as an event's body.
|
||||
//
|
||||
// An event this cannot read is refused by name — which event, which field, why — counted, said in
|
||||
@@ -40,15 +40,16 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// The events, by their local names under the controller's seat.
|
||||
// The events, by their local names under the keeper's seat.
|
||||
const (
|
||||
EventRaised = "condition-raised"
|
||||
EventChanged = "condition-changed"
|
||||
EventCleared = "condition-cleared"
|
||||
// ControllerSeat is the role the events are stated under.
|
||||
ControllerSeat = "mesh-controller"
|
||||
// ConditionSeat is the role the events are stated under, and the verb `conditions` is asked of.
|
||||
ConditionSeat = health.KeeperModule
|
||||
)
|
||||
|
||||
// Severities: two levels, no more (to-be 45 §2).
|
||||
@@ -120,14 +121,11 @@ func (c Condition) SilencedAt(now time.Time) bool {
|
||||
return !c.SilencedTill.IsZero() && now.Before(c.SilencedTill)
|
||||
}
|
||||
|
||||
// eventOf is the local event name of an envelope's key: `mesh-controller.condition-raised` is
|
||||
// `condition-raised`. Anything not of the controller's seat is not a condition event.
|
||||
func eventOf(key string) (string, bool) { return eventOfSeat(key, ControllerSeat) }
|
||||
|
||||
// eventOfSeat is eventOf for the seat the take-over key says is followed: the controller's, or the keeper's (ADR 0564 clause 9).
|
||||
func eventOfSeat(key, seat string) (string, bool) {
|
||||
// eventOf is the local event name of an envelope's key: `mesh-conditions.condition-raised` is
|
||||
// `condition-raised`. Anything not of the keeper's seat is not a condition event.
|
||||
func eventOf(key string) (string, bool) {
|
||||
emitter, event, ok := strings.Cut(key, ".")
|
||||
if !ok || emitter != seat {
|
||||
if !ok || emitter != ConditionSeat {
|
||||
return "", false
|
||||
}
|
||||
switch event {
|
||||
|
||||
@@ -6,19 +6,19 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// The contract with the controller's condition events (to-be 45 §2), as condition.go states it.
|
||||
// The contract with the condition keeper's condition events (to-be 45 §2), as condition.go states it.
|
||||
|
||||
func TestTheEventsAreTheControllersSeat(t *testing.T) {
|
||||
func TestTheEventsAreTheKeepersSeat(t *testing.T) {
|
||||
for key, want := range map[string]string{
|
||||
"mesh-controller.condition-raised": EventRaised,
|
||||
"mesh-controller.condition-changed": EventChanged,
|
||||
"mesh-controller.condition-cleared": EventCleared,
|
||||
"mesh-conditions.condition-raised": EventRaised,
|
||||
"mesh-conditions.condition-changed": EventChanged,
|
||||
"mesh-conditions.condition-cleared": EventCleared,
|
||||
} {
|
||||
if got, ok := eventOf(key); !ok || got != want {
|
||||
t.Errorf("%s: %q %v", key, got, ok)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"mesh-controller.applied", "gitea.condition-raised", "condition-raised"} {
|
||||
for _, key := range []string{"mesh-controller.condition-raised", "mesh-conditions.applied", "gitea.condition-raised", "condition-raised"} {
|
||||
if _, ok := eventOf(key); ok {
|
||||
t.Errorf("%s read as a condition event", key)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func openAt(t *testing.T, backlog []backlogEvent, at time.Time) []Condition {
|
||||
func replay(t *testing.T, h *Holder, backlog []backlogEvent, skip func(Condition, string) bool) {
|
||||
t.Helper()
|
||||
for _, e := range backlog {
|
||||
event, ok := eventOf(ControllerSeat + "." + e.event)
|
||||
event, ok := eventOf(ConditionSeat + "." + e.event)
|
||||
if !ok {
|
||||
t.Fatalf("%s is not a condition event", e.event)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
func errorf(format string, a ...any) error { return fmt.Errorf(format, a...) }
|
||||
@@ -413,12 +412,11 @@ func run(h *Holder, l *listening) {
|
||||
logf("[messenger] %s cannot send: %v", ch.Name(), err)
|
||||
}
|
||||
}
|
||||
// The state first: what is open now is read from the controller before any event is taken, so
|
||||
// The state first: what is open now is read from the keeper before any event is taken, so
|
||||
// what the stream replays is history (novox/hq issue 271). Not reachable yet, it is asked for
|
||||
// again in the loop below, and old events meanwhile are kept as state, never said.
|
||||
follow := newTakeover()
|
||||
h.Start()
|
||||
syncFromController(h, follow)
|
||||
syncFromKeeper(h)
|
||||
go func() {
|
||||
tick := time.NewTicker(5 * time.Second)
|
||||
defer tick.Stop()
|
||||
@@ -431,11 +429,10 @@ func run(h *Holder, l *listening) {
|
||||
}
|
||||
if h.WantsSync() && now.Sub(lastAsk) >= time.Minute {
|
||||
lastAsk = now
|
||||
syncFromController(h, follow)
|
||||
syncFromKeeper(h)
|
||||
}
|
||||
}
|
||||
}()
|
||||
go followTakeover(follow, h)
|
||||
handle := func(e stdio.Envelope) error {
|
||||
if event, ok := strings.CutPrefix(e.Key, ViewModule+"."); ok {
|
||||
// A module in motion on a node, settled or stuck: a walk's start or end now that plans are gone (ADR 0308 §1, ADR 0539 §17).
|
||||
@@ -449,9 +446,8 @@ func run(h *Holder, l *listening) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Only the seat the take-over key names is heard: the other's events are dropped, so the keeper's conditions
|
||||
// are never held a second time beside the controller's (ADR 0564 clause 9).
|
||||
event, ok := eventOfSeat(e.Key, follow.seat())
|
||||
// Only the keeper's condition events are heard (ADR 0564 clause 9; the controller's are no longer consumed).
|
||||
event, ok := eventOf(e.Key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -467,7 +463,7 @@ func run(h *Holder, l *listening) {
|
||||
go takeAsks(h)
|
||||
go answerProofs(h)
|
||||
go followKinds(h)
|
||||
// The delivery view's events say what is in motion (ADR 0539 §17), heard beside the controller's condition events.
|
||||
// The delivery view's events say what is in motion (ADR 0539 §17), heard beside the keeper's condition events.
|
||||
go func() {
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
if err := stdio.Subscribe(ViewModule+".*", handle); err == nil {
|
||||
@@ -480,13 +476,10 @@ func run(h *Holder, l *listening) {
|
||||
}
|
||||
}()
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
err := stdio.Subscribe(ControllerSeat+".*", handle)
|
||||
if err == nil {
|
||||
err = stdio.Subscribe(health.KeeperModule+".*", handle)
|
||||
}
|
||||
err := stdio.Subscribe(ConditionSeat+".*", handle)
|
||||
if err == nil {
|
||||
l.set("listening")
|
||||
logf("[messenger] listening for the controller's condition events")
|
||||
logf("[messenger] listening for the condition keeper's condition events")
|
||||
return
|
||||
}
|
||||
l.set("not yet: " + err.Error())
|
||||
@@ -632,8 +625,8 @@ func followKinds(h *Holder) {
|
||||
}
|
||||
}
|
||||
|
||||
// syncFromController reads the controller's open conditions and gives them to the holder.
|
||||
func syncFromController(h *Holder, follow *takeover) {
|
||||
// syncFromKeeper reads the condition keeper's open conditions and gives them to the holder.
|
||||
func syncFromKeeper(h *Holder) {
|
||||
asked := h.Now()
|
||||
type answer struct {
|
||||
raw json.RawMessage
|
||||
@@ -641,7 +634,7 @@ func syncFromController(h *Holder, follow *takeover) {
|
||||
}
|
||||
done := make(chan answer, 1)
|
||||
go func() {
|
||||
raw, err := stdio.Ask("seat:"+follow.seat()+".conditions", map[string]any{})
|
||||
raw, err := stdio.Ask("seat:"+ConditionSeat+".conditions", map[string]any{})
|
||||
done <- answer{raw, err}
|
||||
}()
|
||||
var a answer
|
||||
|
||||
@@ -80,14 +80,21 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
|
||||
t.Fatalf("uses %v", m.Uses)
|
||||
}
|
||||
if !reflect.DeepEqual(m.Consumes, []string{
|
||||
ControllerSeat + "." + EventRaised, ControllerSeat + "." + EventChanged, ControllerSeat + "." + EventCleared,
|
||||
"mesh-conditions." + EventRaised, "mesh-conditions." + EventChanged, "mesh-conditions." + EventCleared,
|
||||
ViewModule + "." + EventMoving, ViewModule + "." + EventSettled, ViewModule + "." + EventStuck, "intake.choice", "intake.link", "intake.failed", "intake.standing"}) {
|
||||
t.Fatalf("consumes %v", m.Consumes)
|
||||
}
|
||||
// It consumes no mesh-controller event and invokes no mesh-controller verb (774/S12/AC2).
|
||||
for _, list := range [][]string{m.Consumes, m.Invokes} {
|
||||
for _, x := range list {
|
||||
if strings.Contains(x, "mesh-controller") {
|
||||
t.Errorf("%s names the controller", x)
|
||||
}
|
||||
}
|
||||
}
|
||||
// It performs nothing: the verbs it calls read the open conditions, whether root is free, and which issues
|
||||
// track which conditions.
|
||||
if !reflect.DeepEqual(m.Emits, []string{"refused"}) || !reflect.DeepEqual(m.Invokes, []string{"seat:" + ControllerSeat + ".conditions",
|
||||
if !reflect.DeepEqual(m.Emits, []string{"refused"}) || !reflect.DeepEqual(m.Invokes, []string{
|
||||
"seat:mesh-conditions.conditions", "seat:" + EngineSeat + ".root-free", TrackingVerb}) {
|
||||
t.Fatalf("emits %v invokes %v", m.Emits, m.Invokes)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ var previewable = []string{"raised", "reminder", "quiet-reminder", "escalated",
|
||||
func (h *Holder) Preview(key, what string) (map[string]any, error) {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return nil, errorf("key is required: the condition's key, as the controller names it — `mesh-controller.conditions` says which are open")
|
||||
return nil, errorf("key is required: the condition's key, as the keeper names it — `mesh-conditions.conditions` says which are open")
|
||||
}
|
||||
what = strings.TrimSpace(what)
|
||||
if what == "" {
|
||||
@@ -75,7 +75,7 @@ func (h *Holder) Preview(key, what string) (map[string]any, error) {
|
||||
return nil, errorf("%s is not a condition but this router's own %s record, which is said as part of an ask, never on its own; it holds the conditions %s",
|
||||
key, r.Origin, strings.Join(conditions, ", "))
|
||||
case len(conditions) == 0:
|
||||
return nil, errorf("no condition %s is open here, and this router holds none: `mesh-controller.conditions` says what the controller holds open", key)
|
||||
return nil, errorf("no condition %s is open here, and this router holds none: `mesh-conditions.conditions` says what the keeper holds open", key)
|
||||
default:
|
||||
return nil, errorf("no condition %s is open here; it holds %s", key, strings.Join(conditions, ", "))
|
||||
}
|
||||
|
||||
@@ -195,8 +195,9 @@ func TestAPreviewOfWhatThisRouterDoesNotHoldIsRefusedByName(t *testing.T) {
|
||||
t.Errorf("the refusal says neither what was asked nor what is held: %v", err)
|
||||
}
|
||||
|
||||
if _, err := h.Preview("", ""); err == nil || !strings.Contains(err.Error(), "key is required") {
|
||||
t.Errorf("no key: %v", err)
|
||||
if _, err := h.Preview("", ""); err == nil || !strings.Contains(err.Error(), "key is required") ||
|
||||
!strings.Contains(err.Error(), "mesh-conditions.conditions") || strings.Contains(err.Error(), "mesh-controller") {
|
||||
t.Errorf("no key: %v (it says which conditions are open by the keeper's verb, never the controller's; 774/S12/AC1)", err)
|
||||
}
|
||||
if _, err := h.Preview(k.Key, "shouted"); err == nil || !strings.Contains(err.Error(), "shouted") {
|
||||
t.Errorf("an unknown message: %v", err)
|
||||
|
||||
@@ -180,11 +180,3 @@ func (h *Holder) WantsSync() bool {
|
||||
h.init()
|
||||
return h.wantSync || h.syncedAt.IsZero() || h.Now().Sub(h.syncedAt) >= ResyncEvery
|
||||
}
|
||||
|
||||
// AskSync has the next pass of the loop read the open conditions again (the take-over key named another seat).
|
||||
func (h *Holder) AskSync() {
|
||||
h.mu.Lock()
|
||||
h.init()
|
||||
h.wantSync = true
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// takeover is which seat's condition events the messenger hears: the controller's until the keeper's take-over key
|
||||
// says otherwise (novox/hq ADR 0564 clause 9, issue 1952 R1). An absent key follows the controller.
|
||||
type takeover struct {
|
||||
mu sync.Mutex
|
||||
t health.Takeover
|
||||
}
|
||||
|
||||
func newTakeover() *takeover {
|
||||
return &takeover{t: health.Takeover{V: health.TakeoverVersion, Follow: health.FollowController}}
|
||||
}
|
||||
|
||||
func (f *takeover) seat() string { f.mu.Lock(); defer f.mu.Unlock(); return f.t.Seat() }
|
||||
|
||||
// set takes the key as a watch handed it and says whether the followed seat changed.
|
||||
func (f *takeover) set(value []byte, found bool) (changed bool, err error) {
|
||||
t, err := health.DecodeTakeover(value, found)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
changed = f.t.Seat() != t.Seat()
|
||||
f.t = t
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// followTakeover watches the take-over key for the life of the process, and asks the holder to read the followed
|
||||
// seat's open conditions again when it changes. A key it cannot read leaves the seat it followed.
|
||||
func followTakeover(f *takeover, h *Holder) {
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
err := stdio.State(health.KeeperModule+"."+health.TakeoverState).Watch(health.TakeoverKey, func(c stdio.StateChange) error {
|
||||
changed, err := f.set(c.Value, c.Op != "delete" && len(c.Value) > 0)
|
||||
if err != nil {
|
||||
logf("[messenger] the take-over key is unreadable (%v); still hearing %s", err, f.seat())
|
||||
return nil
|
||||
}
|
||||
if changed {
|
||||
logf("[messenger] the take-over key now names %s", f.seat())
|
||||
h.AskSync()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
logf("[messenger] not watching the take-over key yet (%v); hearing %s", err, f.seat())
|
||||
time.Sleep(wait)
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ var examples = []example{{
|
||||
explanation: "Needs you: mend or remove them on shanks, or silence this if they do not matter. On shanks, " +
|
||||
"mnt-recalbox (a mount), storage-media (a mount) and greenclip failed. No module manages them, so the mesh " +
|
||||
"does not repair them.",
|
||||
actions: []Action{{Label: "Silence for a week", Verb: "mesh-controller.conditions",
|
||||
actions: []Action{{Label: "Silence for a week", Verb: "mesh-conditions.conditions",
|
||||
Arguments: map[string]string{"silence": "machine.shanks.units", "for": "7d", "why": "", "cause": "operator-answer"}}},
|
||||
resolved: "No failed services on shanks any more", popup: true,
|
||||
title: "3 failed services on shanks",
|
||||
|
||||
@@ -74,9 +74,6 @@
|
||||
"channel"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-controller.condition-raised",
|
||||
"mesh-controller.condition-changed",
|
||||
"mesh-controller.condition-cleared",
|
||||
"mesh-conditions.condition-raised",
|
||||
"mesh-conditions.condition-changed",
|
||||
"mesh-conditions.condition-cleared",
|
||||
@@ -92,14 +89,10 @@
|
||||
"refused"
|
||||
],
|
||||
"invokes": [
|
||||
"seat:mesh-controller.conditions",
|
||||
"seat:mesh-conditions.conditions",
|
||||
"seat:node-engine.root-free",
|
||||
"seat:issue-tracker.tracking"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-conditions.takeover"
|
||||
],
|
||||
"state": [
|
||||
"open",
|
||||
"sent",
|
||||
|
||||
Reference in New Issue
Block a user