COND7 (the condition's owner): a condition carries the finding's owner and waiting-on; a failed head is the operator's two hours after its since
The keeper copies owner and waiting-on from the finding at raising (and into a condition kept from before G at its next change), hands a pipeline-failed to operator-channel after the setting owner-handover-seconds (default two hours, timed from the finding's since as a bound with its own time to live), says each change as the change owner, and the tool conditions_owner sets an owner now, written in the hand-act log. Builds: design 47 G §540, §685, §678, §557, A-58 (seed-waits: the indexer sets the finding's waiting-on; no kind rule here) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -235,6 +235,9 @@ func (k *Keeper) BoundPassed(key string) error {
|
||||
if strings.HasPrefix(key, askedPrefix) {
|
||||
return k.AskPassed(key)
|
||||
}
|
||||
if strings.HasPrefix(key, ownerPrefix) {
|
||||
return k.OwnerPassed(key)
|
||||
}
|
||||
node, module := splitNodeModule(key)
|
||||
k.mu.Lock()
|
||||
k.init()
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -51,6 +52,16 @@ const (
|
||||
BoundsState = "bounds"
|
||||
)
|
||||
|
||||
// The hand-over of a failed head (design 47 G §540): the condition `pipeline-failed` is its commit's author's, and OwnerHandover
|
||||
// after the finding's `since` it is the operator's, the seat the operator is asked through (a seat's name, §451).
|
||||
const (
|
||||
HandoverKind = "pipeline-failed"
|
||||
HandoverOwner = "operator-channel"
|
||||
OwnerHandover = 2 * time.Hour
|
||||
ownerPrefix = "owner="
|
||||
handedOverWhy = "handed to the operator after %s"
|
||||
)
|
||||
|
||||
// HistoryKeptFor is how long a transition is kept (clause 3).
|
||||
const HistoryKeptFor = 90 * 24 * time.Hour
|
||||
|
||||
@@ -109,6 +120,8 @@ type Keeper struct {
|
||||
Bound time.Duration
|
||||
// AskBound is how long an observer has to state its findings after an ask (the setting ask-bound-seconds, §557); AskedWithin when zero.
|
||||
AskBound time.Duration
|
||||
// Handover is how long a failed head stays its author's (the setting owner-handover-seconds, §557); OwnerHandover when zero.
|
||||
Handover time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
findings map[string]map[string]seat.Finding
|
||||
@@ -366,6 +379,7 @@ func (k *Keeper) settleOnce(key string) error {
|
||||
}
|
||||
k.dropHeld(key, "")
|
||||
next := k.derive(key, was, open, f, now)
|
||||
k.armHandover(next, f, now)
|
||||
events := transitions(was, next, open)
|
||||
if len(events) == 0 && open && sameBody(was, next) {
|
||||
return nil
|
||||
@@ -382,6 +396,9 @@ func (k *Keeper) settleOnce(key string) error {
|
||||
delete(k.cleared, key)
|
||||
k.mu.Unlock()
|
||||
for _, e := range events {
|
||||
if e.Change == cc.ChangeOwner && next.Owner == HandoverOwner {
|
||||
e.Why = fmt.Sprintf(handedOverWhy, k.handover())
|
||||
}
|
||||
e.Condition, e.At, e.Event = next, now, cc.EventFor(e.Change)
|
||||
e.Show = cc.ShowKeeper(next.Key)
|
||||
k.tell(e)
|
||||
@@ -414,6 +431,10 @@ func (k *Keeper) derive(key string, was Condition, open bool, f *seat.Finding, n
|
||||
}
|
||||
k.mu.Unlock()
|
||||
}
|
||||
if !open || c.Owner == "" && c.WaitingOn == "" {
|
||||
c.Owner, c.WaitingOn = f.Owner, f.WaitingOn
|
||||
}
|
||||
k.handOver(&c, *f, now)
|
||||
k.overlay(&c, now)
|
||||
if !open {
|
||||
k.mu.Lock()
|
||||
@@ -428,6 +449,54 @@ func (k *Keeper) derive(key string, was Condition, open bool, f *seat.Finding, n
|
||||
return c
|
||||
}
|
||||
|
||||
// handOver makes a failed head the operator's once its Handover has passed since the finding's `since`: only while it is still
|
||||
// the author's, so a hand's choice of another owner stands (design 47 G §540).
|
||||
func (k *Keeper) handOver(c *Condition, f seat.Finding, now time.Time) {
|
||||
if c.Kind != HandoverKind || c.Owner == "" || c.Owner != f.Owner || c.Owner == HandoverOwner {
|
||||
return
|
||||
}
|
||||
since := f.Since
|
||||
if since.IsZero() {
|
||||
since = f.At
|
||||
}
|
||||
if !now.Before(since.Add(k.handover())) {
|
||||
c.Owner = HandoverOwner
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Keeper) handover() time.Duration {
|
||||
if k.Handover > 0 {
|
||||
return k.Handover
|
||||
}
|
||||
return OwnerHandover
|
||||
}
|
||||
|
||||
// armHandover keeps the end of a failed head's author's time as a bound with its own time to live, so that the keeper hears it
|
||||
// end on the bus and settles the condition again; after a restart the same key stands, or the time has passed and derive
|
||||
// hands it over at once (design 47 G §540: timed from `since`).
|
||||
func (k *Keeper) armHandover(c Condition, f *seat.Finding, now time.Time) {
|
||||
if f == nil || c.Kind != HandoverKind || c.Owner != f.Owner || c.Owner == HandoverOwner {
|
||||
return
|
||||
}
|
||||
since := f.Since
|
||||
if since.IsZero() {
|
||||
since = f.At
|
||||
}
|
||||
ttl := since.Add(k.handover()).Sub(now)
|
||||
if ttl < time.Second {
|
||||
return
|
||||
}
|
||||
if err := k.Store.CreateTTL(BoundsState, ownerPrefix+c.Key, map[string]any{"key": c.Key, "until": since.Add(k.handover())},
|
||||
ttl); err != nil && !errors.Is(err, stdio.ErrChanged) {
|
||||
k.Logf("the hand-over of %s to the operator could not be timed: %v", c.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// OwnerPassed is a failed head's author's time ending: the condition is settled again, which hands it over.
|
||||
func (k *Keeper) OwnerPassed(bound string) error {
|
||||
return k.Settle(strings.TrimPrefix(bound, ownerPrefix))
|
||||
}
|
||||
|
||||
// fromFinding is what the condition says of its finding: its words, subject, severity and source, and its evidence
|
||||
// newest first (clause 2). A healer's escalation is not undone by the finding saying a warning again.
|
||||
func (k *Keeper) fromFinding(c *Condition, f seat.Finding, now time.Time) {
|
||||
@@ -519,6 +588,9 @@ func transitions(was, now Condition, open bool) []Event {
|
||||
case was.Silenced != nil && now.Silenced == nil:
|
||||
out = append(out, Event{Change: cc.ChangeUnsilenced})
|
||||
}
|
||||
if was.Owner != now.Owner {
|
||||
out = append(out, Event{Change: cc.ChangeOwner, Was: was.Owner})
|
||||
}
|
||||
if was.Needs != now.Needs || !sameActions(was.Actions, now.Actions) {
|
||||
out = append(out, Event{Change: cc.ChangeWords, Was: was.Needs})
|
||||
}
|
||||
@@ -626,6 +698,45 @@ func (k *Keeper) Resync() (int, error) {
|
||||
return len(keys), k.settleAll(keys)
|
||||
}
|
||||
|
||||
// SetOwner is the hand act `conditions_owner`: it sets an open condition's owner now, in a form §678 gives, and says the
|
||||
// change as any change of a condition (design 47 G §540, §685).
|
||||
func (k *Keeper) SetOwner(key, owner, by string) (Condition, error) {
|
||||
if !ownerForm.MatchString(owner) {
|
||||
return Condition{}, fmt.Errorf("the owner %q is none of a session who@node, a seat's name or forge:<account> (design 47 G §678)", owner)
|
||||
}
|
||||
for i := 0; i < ConditionWrites; i++ {
|
||||
raw, rev, found, err := k.Store.Get(ConditionsState, key)
|
||||
if err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
if !found || string(raw) == "null" {
|
||||
return Condition{}, fmt.Errorf("no condition %s is open", key)
|
||||
}
|
||||
c, err := decodeCondition(raw)
|
||||
if err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
was := c.Owner
|
||||
if was == owner {
|
||||
return c, nil
|
||||
}
|
||||
now := k.Now().UTC()
|
||||
c.Owner = owner
|
||||
if _, err := k.Store.Update(ConditionsState, key, c.Stored(), rev); errors.Is(err, stdio.ErrChanged) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
k.tell(Event{Condition: c, Event: seat.EventConditionChanged, At: now, Change: cc.ChangeOwner, Was: was,
|
||||
Why: "set by " + by, Show: cc.ShowKeeper(key)})
|
||||
return c, nil
|
||||
}
|
||||
return Condition{}, fmt.Errorf("the condition %s kept moving under the owner change", key)
|
||||
}
|
||||
|
||||
// ownerForm is §678's three forms of an owner: who@node, a seat's name, forge:<account>.
|
||||
var ownerForm = regexp.MustCompile(`^([a-z0-9][a-z0-9._-]*@[a-z0-9][a-z0-9-]*|[a-z][a-z0-9-]*|forge:[A-Za-z0-9._-]+)$`)
|
||||
|
||||
// Silence stops a condition's messages for a while, a hand act with why (clause 3, Limits 1): kept as its transition in
|
||||
// history, naming who and why.
|
||||
func (k *Keeper) Silence(key string, d time.Duration, by, why string) (Condition, error) {
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
}
|
||||
// Clause 12: each state with the tool that reads it, each act with the tool that starts it.
|
||||
readBy := map[string]string{ConditionsState: Seat + ".conditions", HistoryState: HistoryTool,
|
||||
HeldState: HeldTool, BoundsState: HeldTool, ViewsState: FindingsViewsTool, "a resync": Seat + ".resync", "what it holds of observers": FindingsViewsTool, "what it reads": InputsTool}
|
||||
HeldState: HeldTool, BoundsState: HeldTool, ViewsState: FindingsViewsTool, "a resync": Seat + ".resync", "an owner": OwnerTool, "what it holds of observers": FindingsViewsTool, "what it reads": InputsTool}
|
||||
tools := map[string]bool{}
|
||||
for _, tool := range Tools(&Keeper{}) {
|
||||
tools[tool.Name] = true
|
||||
|
||||
@@ -60,6 +60,9 @@ type Condition struct {
|
||||
Count int
|
||||
Tried []Tried
|
||||
Resolver string
|
||||
// Owner and WaitingOn are who the condition is about and what it waits on (design 47 G §685): from the finding when
|
||||
// the condition is raised, and changed only as §540 says.
|
||||
Owner, WaitingOn string
|
||||
// Silenced is nil when no silence is in force.
|
||||
Silenced *Silence
|
||||
Epoch int64
|
||||
@@ -110,7 +113,7 @@ func (c Condition) Stored() seat.ConditionsByKeyValue {
|
||||
Headline: c.Headline, Explanation: c.Explanation, Resolved: c.Resolved, Needs: c.Needs, Actions: c.Actions,
|
||||
Evidence: c.Evidence, Source: c.Source, Raised: c.Raised, LastObserved: c.LastObserved, First: timeP(c.First),
|
||||
Observations: int64(c.Observations), Count: int64(c.Count), Resolver: c.Resolver, Epoch: c.Epoch,
|
||||
LooksUnread: c.LooksUnread}
|
||||
LooksUnread: c.LooksUnread, Owner: c.Owner, WaitingOn: c.WaitingOn}
|
||||
for _, g := range c.Gaps {
|
||||
v.Gaps = append(v.Gaps, seat.ConditionsByKeyValueGapsItem{Cleared: timeP(g.Cleared), Reopened: timeP(g.Reopened)})
|
||||
}
|
||||
@@ -130,7 +133,7 @@ func conditionOf(v seat.ConditionsByKeyValue) Condition {
|
||||
Headline: v.Headline, Explanation: v.Explanation, Resolved: v.Resolved, Needs: v.Needs, Actions: v.Actions,
|
||||
Evidence: v.Evidence, Source: v.Source, Raised: v.Raised, LastObserved: v.LastObserved, First: timeOf(v.First),
|
||||
Observations: int(v.Observations), Count: int(v.Count), Resolver: v.Resolver, Epoch: v.Epoch,
|
||||
LooksUnread: v.LooksUnread}
|
||||
LooksUnread: v.LooksUnread, Owner: v.Owner, WaitingOn: v.WaitingOn}
|
||||
for _, g := range v.Gaps {
|
||||
c.Gaps = append(c.Gaps, Gap{Cleared: timeOf(g.Cleared), Reopened: timeOf(g.Reopened)})
|
||||
}
|
||||
@@ -161,7 +164,7 @@ func (e Event) Said() seat.ConditionEvent {
|
||||
Headline: s.Headline, Explanation: s.Explanation, Resolved: s.Resolved, Needs: s.Needs, Actions: s.Actions,
|
||||
Evidence: s.Evidence, Source: s.Source, Raised: timeP(s.Raised), LastObserved: timeP(s.LastObserved), First: s.First,
|
||||
Observations: s.Observations, Count: s.Count, Resolver: s.Resolver, Epoch: s.Epoch, LooksUnread: s.LooksUnread,
|
||||
Event: e.Event, At: e.At, Change: e.Change, Was: e.Was, Why: e.Why, Cleared: e.Cleared, Show: e.Show}
|
||||
Owner: s.Owner, WaitingOn: s.WaitingOn, Event: e.Event, At: e.At, Change: e.Change, Was: e.Was, Why: e.Why, Cleared: e.Cleared, Show: e.Show}
|
||||
for _, g := range s.Gaps {
|
||||
v.Gaps = append(v.Gaps, seat.ConditionEventGapsItem{Cleared: g.Cleared, Reopened: g.Reopened})
|
||||
}
|
||||
@@ -181,7 +184,7 @@ func eventOf(v seat.ConditionEvent) Event {
|
||||
Headline: v.Headline, Explanation: v.Explanation, Resolved: v.Resolved, Needs: v.Needs, Actions: v.Actions,
|
||||
Evidence: v.Evidence, Source: v.Source, Raised: timeOf(v.Raised), LastObserved: timeOf(v.LastObserved),
|
||||
First: v.First, Observations: v.Observations, Count: v.Count, Resolver: v.Resolver, Epoch: v.Epoch,
|
||||
LooksUnread: v.LooksUnread}
|
||||
LooksUnread: v.LooksUnread, Owner: v.Owner, WaitingOn: v.WaitingOn}
|
||||
if v.Subject != nil {
|
||||
s.Subject = *v.Subject
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
)
|
||||
|
||||
// Design 47 G §540, §685, A-58: a condition carries the finding's owner and waiting-on; a failed head is its author's for two
|
||||
// hours after the finding's `since` and then the operator's, said as the change `owner`; a hand sets an owner in §678's forms;
|
||||
// a newer finding does not undo it.
|
||||
func TestTheKeeperHandsAFailedHeadToTheOperatorAfterTwoHours(t *testing.T) {
|
||||
r := newRig()
|
||||
failed := func(owner string) (string, []byte) {
|
||||
f := seat.Finding{Scope: "pipeline", ID: "abc123", Kind: HandoverKind, Since: r.now, Subject: seat.Subject{Scope: "pipeline", ID: "abc123"},
|
||||
Severity: cc.Warning, Summary: "a head failed", Headline: "failed", Explanation: "It failed.", Source: "runner",
|
||||
Observer: watchman, At: r.now, Owner: owner, WaitingOn: "node-engine", Evidence: []seat.Evidence{{At: r.now, Said: "failed"}}}
|
||||
raw, err := encodeFinding(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cc.Key("pipeline", "abc123", HandoverKind), raw
|
||||
}
|
||||
key, raw := failed("forge:jochen")
|
||||
if err := r.k.Finding(watchman, key, raw, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c, ok := r.open(key); !ok || c.Owner != "forge:jochen" || c.WaitingOn != "node-engine" {
|
||||
t.Fatalf("raised with the finding's owner and waiting-on: %+v", c)
|
||||
}
|
||||
if _, ok := r.st.ttls["bounds/"+ownerPrefix+key]; !ok || r.st.ttls["bounds/"+ownerPrefix+key] != 2*time.Hour {
|
||||
t.Fatalf("the two hours are timed as a bound of their own: %v", r.st.ttls)
|
||||
}
|
||||
r.changes()
|
||||
// An hour in, the finding said again changes nothing; at two hours the bound's end settles it into the operator's.
|
||||
r.now = r.now.Add(time.Hour)
|
||||
if err := r.k.Finding(watchman, key, raw, false); err != nil || r.changes() != "" {
|
||||
t.Fatalf("an hour in nothing changes: %v", err)
|
||||
}
|
||||
r.now = r.now.Add(time.Hour)
|
||||
if err := r.k.BoundPassed(ownerPrefix + key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := r.changes(); got != "owner "+key {
|
||||
t.Fatalf("said %q", got)
|
||||
}
|
||||
if c, _ := r.open(key); c.Owner != HandoverOwner || c.WaitingOn != "node-engine" {
|
||||
t.Fatalf("handed to the operator: %+v", c)
|
||||
}
|
||||
// The finding said again does not give it back to the author, and a restarted keeper that finds the two hours past hands it over alone.
|
||||
if err := r.k.Finding(watchman, key, raw, false); err != nil || r.changes() != "" {
|
||||
t.Fatalf("the hand-over stands: %v", err)
|
||||
}
|
||||
// A hand sets another owner in a form of §678, and refuses a form it does not give.
|
||||
if _, err := r.k.SetOwner(key, "Not An Owner", "jochen"); err == nil {
|
||||
t.Error("a form §678 does not give is refused")
|
||||
}
|
||||
if c, err := r.k.SetOwner(key, "jochen@shanks", "jochen"); err != nil || c.Owner != "jochen@shanks" || r.changes() != "owner "+key {
|
||||
t.Fatalf("a hand's owner: %+v %v", c, err)
|
||||
}
|
||||
if err := r.k.Finding(watchman, key, raw, false); err != nil || r.changes() != "" {
|
||||
t.Fatalf("a hand's owner stands: %v", err)
|
||||
}
|
||||
// A newer failing head is a finding of its own with two hours of its own.
|
||||
r2 := newRig()
|
||||
r2.now = r.now
|
||||
r2.k.Handover = time.Hour
|
||||
other, rawOther := failed("forge:marie")
|
||||
if err := r2.k.Finding(watchman, other, rawOther, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r2.now = r2.now.Add(2 * time.Hour)
|
||||
if err := r2.k.Settle(other); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c, _ := r2.open(other); c.Owner != HandoverOwner {
|
||||
t.Fatalf("a restarted keeper, the time past, hands it over from `since`: %+v", c)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
const (
|
||||
SettingAskBound = "ask-bound-seconds"
|
||||
SettingViewWaits = "view-waits-seconds"
|
||||
SettingHandover = "owner-handover-seconds"
|
||||
defaultViewWaits = 5 * time.Minute
|
||||
)
|
||||
|
||||
@@ -32,5 +33,6 @@ func seconds(read func(key string) (json.RawMessage, error), key string, def tim
|
||||
// ReadSettings sets the keeper's bounds and the first views' wait from the settings.
|
||||
func ReadSettings(k *Keeper, read func(key string) (json.RawMessage, error)) (viewWaits time.Duration) {
|
||||
k.AskBound = seconds(read, SettingAskBound, AskedWithin)
|
||||
k.Handover = seconds(read, SettingHandover, OwnerHandover)
|
||||
return seconds(read, SettingViewWaits, defaultViewWaits)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -21,8 +22,9 @@ import (
|
||||
// Verbs are the seat's verbs, as the manifest claims them.
|
||||
var Verbs = []string{"conditions", "silence", "resync"}
|
||||
|
||||
// The keeper's own read tools.
|
||||
// The keeper's own tools: the owner act, and the read tools.
|
||||
const (
|
||||
OwnerTool = "conditions_owner"
|
||||
HistoryTool = "conditions_history"
|
||||
HeldTool = "conditions_held"
|
||||
InputsTool = "conditions_inputs"
|
||||
@@ -78,6 +80,13 @@ func Tools(k *Keeper) []stdio.Tool {
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
return k.ResyncVerb(stdio.CallerOf(args), strArg(args, "module"))
|
||||
}},
|
||||
{Name: OwnerTool, Description: "Set an open condition's owner now: a session who@node, a seat's name or forge:<account>; " +
|
||||
"said as the condition's change and written in the hand-act log (design 47 G §540).",
|
||||
Input: obj(map[string]any{"key": str("the condition's key"), "owner": str("the new owner"), "why": str("why")},
|
||||
"key", "owner", "why"),
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
return k.OwnerVerb(stdio.CallerOf(args), strArg(args, "key"), strArg(args, "owner"), strArg(args, "why"))
|
||||
}},
|
||||
{Name: FindingsViewsTool, Description: "What the keeper holds of each observer and node: its last stated set with its at, " +
|
||||
"its pending keys and its open asks (design 47 G §390).",
|
||||
Run: func(map[string]any) (any, error) { return k.FindingsViews(), nil }},
|
||||
@@ -98,6 +107,22 @@ func Tools(k *Keeper) []stdio.Tool {
|
||||
}
|
||||
}
|
||||
|
||||
// OwnerVerb is `conditions_owner`: it writes the call in the hand-act log, then sets the owner.
|
||||
func (k *Keeper) OwnerVerb(by, key, owner, why string) (any, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("an owner change says why")
|
||||
}
|
||||
record := k.Record
|
||||
if record == nil {
|
||||
record = recordHandAct
|
||||
}
|
||||
if err := record("set the owner of "+key+" to "+owner, why+" (by "+by+")", OwnerTool); err != nil {
|
||||
return nil, fmt.Errorf("the hand act was not recorded, so nothing was changed: %w", err)
|
||||
}
|
||||
c, err := k.SetOwner(key, owner, by)
|
||||
return c.Stored(), err
|
||||
}
|
||||
|
||||
// ConditionsVerb is `conditions`: the list, one whole, or the history.
|
||||
func (k *Keeper) ConditionsVerb(args map[string]any) (any, error) {
|
||||
key := strArg(args, "key")
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
"minimum": 1,
|
||||
"default": 300
|
||||
},
|
||||
"owner-handover-seconds": {
|
||||
"type": "number",
|
||||
"description": "How long a failed head stays its commit's author's, in seconds, before the keeper hands its condition to the operator: 7200 is two hours.",
|
||||
"minimum": 1,
|
||||
"default": 7200
|
||||
},
|
||||
"view-waits-seconds": {
|
||||
"type": "number",
|
||||
"description": "How long a first view may stay unanswered, in seconds, before the keeper raises view-waits for it: 300 is five minutes.",
|
||||
|
||||
Reference in New Issue
Block a user