diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go index 61e93a98..bec6846f 100644 --- a/contracts/go/seats/nodebuildgo/contract.go +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -18,6 +18,7 @@ const ( VerbCurrent = "current" VerbPause = "pause" VerbResume = "resume" + VerbDrop = "drop" ) const ( @@ -28,7 +29,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbCurrent, VerbPause, VerbResume} + return []string{VerbCurrent, VerbPause, VerbResume, VerbDrop} } // AcceptSubjectDelivery is where delivery is sent. @@ -117,6 +118,41 @@ func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) return v, nil } +// PausesByDroppedIDKey is the key of one value of the state pauses under the form "dropped.". +func PausesByDroppedIDKey(id string) string { return "dropped." + id } + +// ParsePausesByDroppedIDKey reads the parts of a key of the form "dropped.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByDroppedIDKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "dropped.") { + return "", false + } + rest = rest[8:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByDroppedIDValue reads one value of the state pauses under the form "dropped.", refusing a field PausesByDroppedIDValue does not know and any text after the value. +func DecodePausesByDroppedIDValue(raw []byte) (PausesByDroppedIDValue, error) { + var v PausesByDroppedIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.". func PausesByCapacityNodeKey(node string) string { return "capacity." + node } @@ -245,6 +281,16 @@ type ComposeAnswer struct { Verdict any `json:"verdict,omitempty"` } +// Drop is an ask a caller no longer waits for. +type Drop struct { + // when (RFC 3339 time) + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` + Why string `json:"why"` +} + // FetchBesideItem is a type of the contract. type FetchBesideItem struct { Commit string `json:"commit"` @@ -433,19 +479,25 @@ type RunProgram struct { // Step is one step a build module runs on a node. type Step struct { + // when the bus gives the ask again, for the state again (RFC 3339 time) + AgainAt *time.Time `json:"again-at,omitempty"` // when (RFC 3339 time) Ended *time.Time `json:"ended,omitempty"` ID string `json:"id"` Node string `json:"node"` Owner string `json:"owner,omitempty"` - Phase string `json:"phase,omitempty"` + // true when the step ended on a fault that passes (a transport fault), so a failed end after the last run says so + Passing bool `json:"passing,omitempty"` + Phase string `json:"phase,omitempty"` // when (RFC 3339 time) PhaseAt *time.Time `json:"phase-at,omitempty"` Reason string `json:"reason,omitempty"` Result *StepResult `json:"result,omitempty"` + // which run of the ask this is, counted by the bus's delivery count (1 for the first) + Run int64 `json:"run,omitempty"` // when (RFC 3339 time) Started time.Time `json:"started"` - // running, passed, failed or refused + // running, again (ended on a transport fault, to be given again by the bus after again-at), passed, failed or refused State string `json:"state"` Tool string `json:"tool"` // the layout version @@ -588,6 +640,19 @@ type ResumeInput struct { // ResumeAnswer is what resume answers. type ResumeAnswer = map[string]any +// DropInput is what drop takes. +type DropInput struct { + // who drops it + By string `json:"by,omitempty"` + // the ask's id + ID string `json:"id"` + // why the caller no longer waits + Why string `json:"why"` +} + +// DropAnswer is what drop answers. +type DropAnswer = map[string]any + // DeliveryAccept is the body of delivery. type DeliveryAccept = StepAsk @@ -603,6 +668,9 @@ type StepsByNodeIDValue = Step // PausesByPausedNodeValue is one value of the state pauses under the key paused.. type PausesByPausedNodeValue = Pause +// PausesByDroppedIDValue is one value of the state pauses under the key dropped.. +type PausesByDroppedIDValue = Drop + // PausesByCapacityNodeValue is one value of the state pauses under the key capacity.. type PausesByCapacityNodeValue = Capacity diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go index f120459d..2750cb84 100644 --- a/contracts/go/seats/nodebuildgo/contract_test.go +++ b/contracts/go/seats/nodebuildgo/contract_test.go @@ -53,6 +53,12 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")}, {name: "a finished run step whose program exited non-zero", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"r5e6\",\"node\":\"ace\",\"result\":{\"built\":true,\"exit\":2,\"key\":\"program-0123456789abcdef\",\"said\":\"the facts name no head\"},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"failed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"r5e6\",\"node\":\"ace\",\"result\":{\"built\":true,\"exit\":2,\"key\":\"program-0123456789abcdef\",\"said\":\"the facts name no head\"},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"failed\",\"tool\":\"check\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a step ended on a transport fault, given again by the bus", subject: "", given: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", wire: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "an ask dropped by the caller that no longer waits", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", + decode: func(b []byte) (any, error) { var v PausesByDroppedIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", + decode: func(b []byte) (any, error) { var v DropInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/contracts/go/seats/nodebuildimage/contract.go b/contracts/go/seats/nodebuildimage/contract.go index 8ea6e02f..11c36d89 100644 --- a/contracts/go/seats/nodebuildimage/contract.go +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -18,6 +18,7 @@ const ( VerbCurrent = "current" VerbPause = "pause" VerbResume = "resume" + VerbDrop = "drop" ) const ( @@ -28,7 +29,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbCurrent, VerbPause, VerbResume} + return []string{VerbCurrent, VerbPause, VerbResume, VerbDrop} } // AcceptSubjectDelivery is where delivery is sent. @@ -117,6 +118,41 @@ func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) return v, nil } +// PausesByDroppedIDKey is the key of one value of the state pauses under the form "dropped.". +func PausesByDroppedIDKey(id string) string { return "dropped." + id } + +// ParsePausesByDroppedIDKey reads the parts of a key of the form "dropped.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByDroppedIDKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "dropped.") { + return "", false + } + rest = rest[8:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByDroppedIDValue reads one value of the state pauses under the form "dropped.", refusing a field PausesByDroppedIDValue does not know and any text after the value. +func DecodePausesByDroppedIDValue(raw []byte) (PausesByDroppedIDValue, error) { + var v PausesByDroppedIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.". func PausesByCapacityNodeKey(node string) string { return "capacity." + node } @@ -245,6 +281,16 @@ type ComposeAnswer struct { Verdict any `json:"verdict,omitempty"` } +// Drop is an ask a caller no longer waits for. +type Drop struct { + // when (RFC 3339 time) + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` + Why string `json:"why"` +} + // FetchBesideItem is a type of the contract. type FetchBesideItem struct { Commit string `json:"commit"` @@ -433,19 +479,25 @@ type RunProgram struct { // Step is one step a build module runs on a node. type Step struct { + // when the bus gives the ask again, for the state again (RFC 3339 time) + AgainAt *time.Time `json:"again-at,omitempty"` // when (RFC 3339 time) Ended *time.Time `json:"ended,omitempty"` ID string `json:"id"` Node string `json:"node"` Owner string `json:"owner,omitempty"` - Phase string `json:"phase,omitempty"` + // true when the step ended on a fault that passes (a transport fault), so a failed end after the last run says so + Passing bool `json:"passing,omitempty"` + Phase string `json:"phase,omitempty"` // when (RFC 3339 time) PhaseAt *time.Time `json:"phase-at,omitempty"` Reason string `json:"reason,omitempty"` Result *StepResult `json:"result,omitempty"` + // which run of the ask this is, counted by the bus's delivery count (1 for the first) + Run int64 `json:"run,omitempty"` // when (RFC 3339 time) Started time.Time `json:"started"` - // running, passed, failed or refused + // running, again (ended on a transport fault, to be given again by the bus after again-at), passed, failed or refused State string `json:"state"` Tool string `json:"tool"` // the layout version @@ -588,6 +640,19 @@ type ResumeInput struct { // ResumeAnswer is what resume answers. type ResumeAnswer = map[string]any +// DropInput is what drop takes. +type DropInput struct { + // who drops it + By string `json:"by,omitempty"` + // the ask's id + ID string `json:"id"` + // why the caller no longer waits + Why string `json:"why"` +} + +// DropAnswer is what drop answers. +type DropAnswer = map[string]any + // DeliveryAccept is the body of delivery. type DeliveryAccept = StepAsk @@ -603,6 +668,9 @@ type StepsByNodeIDValue = Step // PausesByPausedNodeValue is one value of the state pauses under the key paused.. type PausesByPausedNodeValue = Pause +// PausesByDroppedIDValue is one value of the state pauses under the key dropped.. +type PausesByDroppedIDValue = Drop + // PausesByCapacityNodeValue is one value of the state pauses under the key capacity.. type PausesByCapacityNodeValue = Capacity diff --git a/contracts/go/seats/nodebuildimage/contract_test.go b/contracts/go/seats/nodebuildimage/contract_test.go index 75649a4a..2926f492 100644 --- a/contracts/go/seats/nodebuildimage/contract_test.go +++ b/contracts/go/seats/nodebuildimage/contract_test.go @@ -39,6 +39,12 @@ func TestFixtures(t *testing.T) { }, subjectIs: nil}, {name: "a build ask", subject: "mesh.seat.node-build-image.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.accept\\.delivery$")}, + {name: "a step ended on a transport fault, given again by the bus", subject: "", given: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", wire: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "an ask dropped by the caller that no longer waits", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", + decode: func(b []byte) (any, error) { var v PausesByDroppedIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", + decode: func(b []byte) (any, error) { var v DropInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/contracts/go/seats/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go index b12f7595..07404adf 100644 --- a/contracts/go/seats/nodebuildnode/contract.go +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -18,6 +18,7 @@ const ( VerbCurrent = "current" VerbPause = "pause" VerbResume = "resume" + VerbDrop = "drop" ) const ( @@ -28,7 +29,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbCurrent, VerbPause, VerbResume} + return []string{VerbCurrent, VerbPause, VerbResume, VerbDrop} } // AcceptSubjectDelivery is where delivery is sent. @@ -117,6 +118,41 @@ func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) return v, nil } +// PausesByDroppedIDKey is the key of one value of the state pauses under the form "dropped.". +func PausesByDroppedIDKey(id string) string { return "dropped." + id } + +// ParsePausesByDroppedIDKey reads the parts of a key of the form "dropped.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByDroppedIDKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "dropped.") { + return "", false + } + rest = rest[8:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByDroppedIDValue reads one value of the state pauses under the form "dropped.", refusing a field PausesByDroppedIDValue does not know and any text after the value. +func DecodePausesByDroppedIDValue(raw []byte) (PausesByDroppedIDValue, error) { + var v PausesByDroppedIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.". func PausesByCapacityNodeKey(node string) string { return "capacity." + node } @@ -245,6 +281,16 @@ type ComposeAnswer struct { Verdict any `json:"verdict,omitempty"` } +// Drop is an ask a caller no longer waits for. +type Drop struct { + // when (RFC 3339 time) + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` + Why string `json:"why"` +} + // FetchBesideItem is a type of the contract. type FetchBesideItem struct { Commit string `json:"commit"` @@ -433,19 +479,25 @@ type RunProgram struct { // Step is one step a build module runs on a node. type Step struct { + // when the bus gives the ask again, for the state again (RFC 3339 time) + AgainAt *time.Time `json:"again-at,omitempty"` // when (RFC 3339 time) Ended *time.Time `json:"ended,omitempty"` ID string `json:"id"` Node string `json:"node"` Owner string `json:"owner,omitempty"` - Phase string `json:"phase,omitempty"` + // true when the step ended on a fault that passes (a transport fault), so a failed end after the last run says so + Passing bool `json:"passing,omitempty"` + Phase string `json:"phase,omitempty"` // when (RFC 3339 time) PhaseAt *time.Time `json:"phase-at,omitempty"` Reason string `json:"reason,omitempty"` Result *StepResult `json:"result,omitempty"` + // which run of the ask this is, counted by the bus's delivery count (1 for the first) + Run int64 `json:"run,omitempty"` // when (RFC 3339 time) Started time.Time `json:"started"` - // running, passed, failed or refused + // running, again (ended on a transport fault, to be given again by the bus after again-at), passed, failed or refused State string `json:"state"` Tool string `json:"tool"` // the layout version @@ -588,6 +640,19 @@ type ResumeInput struct { // ResumeAnswer is what resume answers. type ResumeAnswer = map[string]any +// DropInput is what drop takes. +type DropInput struct { + // who drops it + By string `json:"by,omitempty"` + // the ask's id + ID string `json:"id"` + // why the caller no longer waits + Why string `json:"why"` +} + +// DropAnswer is what drop answers. +type DropAnswer = map[string]any + // DeliveryAccept is the body of delivery. type DeliveryAccept = StepAsk @@ -603,6 +668,9 @@ type StepsByNodeIDValue = Step // PausesByPausedNodeValue is one value of the state pauses under the key paused.. type PausesByPausedNodeValue = Pause +// PausesByDroppedIDValue is one value of the state pauses under the key dropped.. +type PausesByDroppedIDValue = Drop + // PausesByCapacityNodeValue is one value of the state pauses under the key capacity.. type PausesByCapacityNodeValue = Capacity diff --git a/contracts/go/seats/nodebuildnode/contract_test.go b/contracts/go/seats/nodebuildnode/contract_test.go index fd58eeb3..f8bedce6 100644 --- a/contracts/go/seats/nodebuildnode/contract_test.go +++ b/contracts/go/seats/nodebuildnode/contract_test.go @@ -43,6 +43,12 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.accept\\.check$")}, {name: "a finished check step of a TypeScript merge preview", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a step ended on a transport fault, given again by the bus", subject: "", given: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", wire: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "an ask dropped by the caller that no longer waits", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", + decode: func(b []byte) (any, error) { var v PausesByDroppedIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", + decode: func(b []byte) (any, error) { var v DropInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/contracts/parts/build-step.json b/contracts/parts/build-step.json index 9dc9c2ed..faf2d6a3 100644 --- a/contracts/parts/build-step.json +++ b/contracts/parts/build-step.json @@ -38,6 +38,36 @@ "type": "object", "description": "whether the node is paused" } + }, + { + "name": "drop", + "description": "End an ask this node holds for a caller that no longer waits for it: a step not yet running is refused when it is pulled, one running is cancelled and ends refused, naming who dropped it and why. Writes dropped. in the pauses state; the build module removes the key when it has acted.", + "callers": "the build seats' asker (the builder and the checker)", + "input": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the ask's id" + }, + "by": { + "type": "string", + "description": "who drops it" + }, + "why": { + "type": "string", + "description": "why the caller no longer waits" + } + }, + "required": [ + "id", + "why" + ] + }, + "answer": { + "type": "object", + "description": "whether the ask was held here" + } } ], "defs": { @@ -404,7 +434,7 @@ }, "state": { "type": "string", - "description": "running, passed, failed or refused" + "description": "running, again (ended on a transport fault, to be given again by the bus after again-at), passed, failed or refused" }, "started": { "type": "string", @@ -435,6 +465,19 @@ }, "owner": { "type": "string" + }, + "run": { + "type": "integer", + "description": "which run of the ask this is, counted by the bus's delivery count (1 for the first)" + }, + "again-at": { + "type": "string", + "description": "when the bus gives the ask again, for the state again (RFC 3339 time)", + "format": "date-time" + }, + "passing": { + "type": "boolean", + "description": "true when the step ended on a fault that passes (a transport fault), so a failed end after the last run says so" } }, "required": [ @@ -476,6 +519,30 @@ ], "description": "the toolchain a node reports" }, + "Drop": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)", + "format": "date-time" + } + }, + "required": [ + "why" + ], + "description": "an ask a caller no longer waits for" + }, "Pause": { "type": "object", "properties": { diff --git a/contracts/seats/node-build-go/fixtures/states.json b/contracts/seats/node-build-go/fixtures/states.json index bde9d6d6..9af3c52c 100644 --- a/contracts/seats/node-build-go/fixtures/states.json +++ b/contracts/seats/node-build-go/fixtures/states.json @@ -312,5 +312,32 @@ "said": "the facts name no head" } } + }, + { + "name": "a step ended on a transport fault, given again by the bus", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "again", + "started": "2026-11-05T10:00:00Z", + "ended": "2026-11-05T10:00:30Z", + "reason": "fetching the commit: kex_exchange_identification: read: Connection reset by peer", + "run": 1, + "again-at": "2026-11-05T10:01:30Z", + "passing": true + } + }, + { + "name": "an ask dropped by the caller that no longer waits", + "of": "state:pauses/dropped.", + "given": { + "v": 1, + "by": "mesh-delivery-builder", + "why": "a newer fingerprint supersedes it", + "at": "2026-11-05T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-go/fixtures/verbs.json b/contracts/seats/node-build-go/fixtures/verbs.json new file mode 100644 index 00000000..727e95b6 --- /dev/null +++ b/contracts/seats/node-build-go/fixtures/verbs.json @@ -0,0 +1,11 @@ +[ + { + "name": "drop names the ask and why", + "of": "verb:drop.input", + "given": { + "id": "a1b2", + "by": "mesh-delivery-builder", + "why": "a newer fingerprint supersedes it" + } + } +] diff --git a/contracts/seats/node-build-go/seat.json b/contracts/seats/node-build-go/seat.json index a810a6bb..ccdd85cd 100644 --- a/contracts/seats/node-build-go/seat.json +++ b/contracts/seats/node-build-go/seat.json @@ -58,6 +58,13 @@ }, "description": "whether a node is paused" }, + { + "key": "dropped.", + "value": { + "$ref": "#/defs/Drop" + }, + "description": "an ask its caller no longer waits for" + }, { "key": "capacity.", "value": { diff --git a/contracts/seats/node-build-image/fixtures/states.json b/contracts/seats/node-build-image/fixtures/states.json index b49e8c53..7e3ef453 100644 --- a/contracts/seats/node-build-image/fixtures/states.json +++ b/contracts/seats/node-build-image/fixtures/states.json @@ -59,5 +59,32 @@ ] } } + }, + { + "name": "a step ended on a transport fault, given again by the bus", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "again", + "started": "2026-11-05T10:00:00Z", + "ended": "2026-11-05T10:00:30Z", + "reason": "fetching the commit: kex_exchange_identification: read: Connection reset by peer", + "run": 1, + "again-at": "2026-11-05T10:01:30Z", + "passing": true + } + }, + { + "name": "an ask dropped by the caller that no longer waits", + "of": "state:pauses/dropped.", + "given": { + "v": 1, + "by": "mesh-delivery-builder", + "why": "a newer fingerprint supersedes it", + "at": "2026-11-05T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-image/fixtures/verbs.json b/contracts/seats/node-build-image/fixtures/verbs.json new file mode 100644 index 00000000..727e95b6 --- /dev/null +++ b/contracts/seats/node-build-image/fixtures/verbs.json @@ -0,0 +1,11 @@ +[ + { + "name": "drop names the ask and why", + "of": "verb:drop.input", + "given": { + "id": "a1b2", + "by": "mesh-delivery-builder", + "why": "a newer fingerprint supersedes it" + } + } +] diff --git a/contracts/seats/node-build-image/seat.json b/contracts/seats/node-build-image/seat.json index 037a8907..60295d98 100644 --- a/contracts/seats/node-build-image/seat.json +++ b/contracts/seats/node-build-image/seat.json @@ -58,6 +58,13 @@ }, "description": "whether a node is paused" }, + { + "key": "dropped.", + "value": { + "$ref": "#/defs/Drop" + }, + "description": "an ask its caller no longer waits for" + }, { "key": "capacity.", "value": { diff --git a/contracts/seats/node-build-node/fixtures/states.json b/contracts/seats/node-build-node/fixtures/states.json index 125819d0..d8e5ea20 100644 --- a/contracts/seats/node-build-node/fixtures/states.json +++ b/contracts/seats/node-build-node/fixtures/states.json @@ -131,5 +131,32 @@ } } } + }, + { + "name": "a step ended on a transport fault, given again by the bus", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "again", + "started": "2026-11-05T10:00:00Z", + "ended": "2026-11-05T10:00:30Z", + "reason": "fetching the commit: kex_exchange_identification: read: Connection reset by peer", + "run": 1, + "again-at": "2026-11-05T10:01:30Z", + "passing": true + } + }, + { + "name": "an ask dropped by the caller that no longer waits", + "of": "state:pauses/dropped.", + "given": { + "v": 1, + "by": "mesh-delivery-builder", + "why": "a newer fingerprint supersedes it", + "at": "2026-11-05T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-node/fixtures/verbs.json b/contracts/seats/node-build-node/fixtures/verbs.json new file mode 100644 index 00000000..727e95b6 --- /dev/null +++ b/contracts/seats/node-build-node/fixtures/verbs.json @@ -0,0 +1,11 @@ +[ + { + "name": "drop names the ask and why", + "of": "verb:drop.input", + "given": { + "id": "a1b2", + "by": "mesh-delivery-builder", + "why": "a newer fingerprint supersedes it" + } + } +] diff --git a/contracts/seats/node-build-node/seat.json b/contracts/seats/node-build-node/seat.json index 6fce1468..74873ba1 100644 --- a/contracts/seats/node-build-node/seat.json +++ b/contracts/seats/node-build-node/seat.json @@ -58,6 +58,13 @@ }, "description": "whether a node is paused" }, + { + "key": "dropped.", + "value": { + "$ref": "#/defs/Drop" + }, + "description": "an ask its caller no longer waits for" + }, { "key": "capacity.", "value": { diff --git a/contracts/ts/seats/nodebuildgo/src/index.ts b/contracts/ts/seats/nodebuildgo/src/index.ts index 0c6834dc..a465a45d 100644 --- a/contracts/ts/seats/nodebuildgo/src/index.ts +++ b/contracts/ts/seats/nodebuildgo/src/index.ts @@ -10,6 +10,7 @@ export const verbs = { current: "current", pause: "pause", resume: "resume", + drop: "drop", } as const; export const accepts = { @@ -66,6 +67,23 @@ export function parsePausesByPausedNodeKey(_key: string): { node: string } | und return { node }; } +/** The key of one value of the state pauses under the form "dropped.". */ +export function pausesByDroppedIDKey(id: string): string { + return "dropped." + id; +} + +/** The parts of a key of the form "dropped.", or undefined for a key of another form. */ +export function parsePausesByDroppedIDKey(_key: string): { id: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("dropped.")) return undefined; + _rest = _rest.slice(8); + if (_rest === "") return undefined; + const id = _rest; + _rest = ""; + if (_rest !== "") return undefined; + return { id }; +} + /** The key of one value of the state pauses under the form "capacity.". */ export function pausesByCapacityNodeKey(node: string): string { return "capacity." + node; @@ -110,6 +128,9 @@ export type CheckAnswer = buildStep.CheckAnswer; export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; +export type Drop = buildStep.Drop; +export type DropAnswer = buildStep.DropAnswer; +export type DropInput = buildStep.DropInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; @@ -159,6 +180,9 @@ export type StepsByNodeIDValue = Step; /** is one value of the state pauses under the key paused.. */ export type PausesByPausedNodeValue = Pause; +/** is one value of the state pauses under the key dropped.. */ +export type PausesByDroppedIDValue = Drop; + /** is one value of the state pauses under the key capacity.. */ export type PausesByCapacityNodeValue = Capacity; diff --git a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts index 1aee13ff..c3d1c882 100644 --- a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts @@ -6,6 +6,7 @@ export const verbs = { current: "current", pause: "pause", resume: "resume", + drop: "drop", } as const; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -62,6 +63,16 @@ export interface ComposeAnswer { "verdict"?: unknown; } +/** an ask a caller no longer waits for */ +export interface Drop { + /** when (RFC 3339 time) */ + "at"?: string; + "by"?: string; + /** the layout version */ + "v"?: number; + "why": string; +} + export interface FetchBesideItem { "commit": string; "repository": string; @@ -244,19 +255,25 @@ export interface RunProgram { /** one step a build module runs on a node */ export interface Step { + /** when the bus gives the ask again, for the state again (RFC 3339 time) */ + "again-at"?: string; /** when (RFC 3339 time) */ "ended"?: string; "id": string; "node": string; "owner"?: string; + /** true when the step ended on a fault that passes (a transport fault), so a failed end after the last run says so */ + "passing"?: boolean; "phase"?: string; /** when (RFC 3339 time) */ "phase-at"?: string; "reason"?: string; "result"?: StepResult; + /** which run of the ask this is, counted by the bus's delivery count (1 for the first) */ + "run"?: number; /** when (RFC 3339 time) */ "started": string; - /** running, passed, failed or refused */ + /** running, again (ended on a transport fault, to be given again by the bus after again-at), passed, failed or refused */ "state": string; "tool": string; /** the layout version */ @@ -396,3 +413,16 @@ export interface ResumeInput { /** is what resume answers. */ export type ResumeAnswer = Record; +/** is what drop takes. */ +export interface DropInput { + /** who drops it */ + "by"?: string; + /** the ask's id */ + "id": string; + /** why the caller no longer waits */ + "why": string; +} + +/** is what drop answers. */ +export type DropAnswer = Record; + diff --git a/contracts/ts/seats/nodebuildimage/src/index.ts b/contracts/ts/seats/nodebuildimage/src/index.ts index 91e5d235..8024a0d0 100644 --- a/contracts/ts/seats/nodebuildimage/src/index.ts +++ b/contracts/ts/seats/nodebuildimage/src/index.ts @@ -10,6 +10,7 @@ export const verbs = { current: "current", pause: "pause", resume: "resume", + drop: "drop", } as const; export const accepts = { @@ -66,6 +67,23 @@ export function parsePausesByPausedNodeKey(_key: string): { node: string } | und return { node }; } +/** The key of one value of the state pauses under the form "dropped.". */ +export function pausesByDroppedIDKey(id: string): string { + return "dropped." + id; +} + +/** The parts of a key of the form "dropped.", or undefined for a key of another form. */ +export function parsePausesByDroppedIDKey(_key: string): { id: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("dropped.")) return undefined; + _rest = _rest.slice(8); + if (_rest === "") return undefined; + const id = _rest; + _rest = ""; + if (_rest !== "") return undefined; + return { id }; +} + /** The key of one value of the state pauses under the form "capacity.". */ export function pausesByCapacityNodeKey(node: string): string { return "capacity." + node; @@ -110,6 +128,9 @@ export type CheckAnswer = buildStep.CheckAnswer; export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; +export type Drop = buildStep.Drop; +export type DropAnswer = buildStep.DropAnswer; +export type DropInput = buildStep.DropInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; @@ -159,6 +180,9 @@ export type StepsByNodeIDValue = Step; /** is one value of the state pauses under the key paused.. */ export type PausesByPausedNodeValue = Pause; +/** is one value of the state pauses under the key dropped.. */ +export type PausesByDroppedIDValue = Drop; + /** is one value of the state pauses under the key capacity.. */ export type PausesByCapacityNodeValue = Capacity; diff --git a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts index 1aee13ff..c3d1c882 100644 --- a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts @@ -6,6 +6,7 @@ export const verbs = { current: "current", pause: "pause", resume: "resume", + drop: "drop", } as const; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -62,6 +63,16 @@ export interface ComposeAnswer { "verdict"?: unknown; } +/** an ask a caller no longer waits for */ +export interface Drop { + /** when (RFC 3339 time) */ + "at"?: string; + "by"?: string; + /** the layout version */ + "v"?: number; + "why": string; +} + export interface FetchBesideItem { "commit": string; "repository": string; @@ -244,19 +255,25 @@ export interface RunProgram { /** one step a build module runs on a node */ export interface Step { + /** when the bus gives the ask again, for the state again (RFC 3339 time) */ + "again-at"?: string; /** when (RFC 3339 time) */ "ended"?: string; "id": string; "node": string; "owner"?: string; + /** true when the step ended on a fault that passes (a transport fault), so a failed end after the last run says so */ + "passing"?: boolean; "phase"?: string; /** when (RFC 3339 time) */ "phase-at"?: string; "reason"?: string; "result"?: StepResult; + /** which run of the ask this is, counted by the bus's delivery count (1 for the first) */ + "run"?: number; /** when (RFC 3339 time) */ "started": string; - /** running, passed, failed or refused */ + /** running, again (ended on a transport fault, to be given again by the bus after again-at), passed, failed or refused */ "state": string; "tool": string; /** the layout version */ @@ -396,3 +413,16 @@ export interface ResumeInput { /** is what resume answers. */ export type ResumeAnswer = Record; +/** is what drop takes. */ +export interface DropInput { + /** who drops it */ + "by"?: string; + /** the ask's id */ + "id": string; + /** why the caller no longer waits */ + "why": string; +} + +/** is what drop answers. */ +export type DropAnswer = Record; + diff --git a/contracts/ts/seats/nodebuildnode/src/index.ts b/contracts/ts/seats/nodebuildnode/src/index.ts index faba1e6a..85ec77b6 100644 --- a/contracts/ts/seats/nodebuildnode/src/index.ts +++ b/contracts/ts/seats/nodebuildnode/src/index.ts @@ -10,6 +10,7 @@ export const verbs = { current: "current", pause: "pause", resume: "resume", + drop: "drop", } as const; export const accepts = { @@ -66,6 +67,23 @@ export function parsePausesByPausedNodeKey(_key: string): { node: string } | und return { node }; } +/** The key of one value of the state pauses under the form "dropped.". */ +export function pausesByDroppedIDKey(id: string): string { + return "dropped." + id; +} + +/** The parts of a key of the form "dropped.", or undefined for a key of another form. */ +export function parsePausesByDroppedIDKey(_key: string): { id: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("dropped.")) return undefined; + _rest = _rest.slice(8); + if (_rest === "") return undefined; + const id = _rest; + _rest = ""; + if (_rest !== "") return undefined; + return { id }; +} + /** The key of one value of the state pauses under the form "capacity.". */ export function pausesByCapacityNodeKey(node: string): string { return "capacity." + node; @@ -110,6 +128,9 @@ export type CheckAnswer = buildStep.CheckAnswer; export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; +export type Drop = buildStep.Drop; +export type DropAnswer = buildStep.DropAnswer; +export type DropInput = buildStep.DropInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; @@ -159,6 +180,9 @@ export type StepsByNodeIDValue = Step; /** is one value of the state pauses under the key paused.. */ export type PausesByPausedNodeValue = Pause; +/** is one value of the state pauses under the key dropped.. */ +export type PausesByDroppedIDValue = Drop; + /** is one value of the state pauses under the key capacity.. */ export type PausesByCapacityNodeValue = Capacity; diff --git a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts index 1aee13ff..c3d1c882 100644 --- a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts @@ -6,6 +6,7 @@ export const verbs = { current: "current", pause: "pause", resume: "resume", + drop: "drop", } as const; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -62,6 +63,16 @@ export interface ComposeAnswer { "verdict"?: unknown; } +/** an ask a caller no longer waits for */ +export interface Drop { + /** when (RFC 3339 time) */ + "at"?: string; + "by"?: string; + /** the layout version */ + "v"?: number; + "why": string; +} + export interface FetchBesideItem { "commit": string; "repository": string; @@ -244,19 +255,25 @@ export interface RunProgram { /** one step a build module runs on a node */ export interface Step { + /** when the bus gives the ask again, for the state again (RFC 3339 time) */ + "again-at"?: string; /** when (RFC 3339 time) */ "ended"?: string; "id": string; "node": string; "owner"?: string; + /** true when the step ended on a fault that passes (a transport fault), so a failed end after the last run says so */ + "passing"?: boolean; "phase"?: string; /** when (RFC 3339 time) */ "phase-at"?: string; "reason"?: string; "result"?: StepResult; + /** which run of the ask this is, counted by the bus's delivery count (1 for the first) */ + "run"?: number; /** when (RFC 3339 time) */ "started": string; - /** running, passed, failed or refused */ + /** running, again (ended on a transport fault, to be given again by the bus after again-at), passed, failed or refused */ "state": string; "tool": string; /** the layout version */ @@ -396,3 +413,16 @@ export interface ResumeInput { /** is what resume answers. */ export type ResumeAnswer = Record; +/** is what drop takes. */ +export interface DropInput { + /** who drops it */ + "by"?: string; + /** the ask's id */ + "id": string; + /** why the caller no longer waits */ + "why": string; +} + +/** is what drop answers. */ +export type DropAnswer = Record; +