Repository: akuity/kargo
Pull request #3068 (2024-12-05 20:23:17)

chore: manual backport of refactored retries (#3060)
Manual backport of #3060 due to merge conflicts in generated code.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/akuity/kargo/pull/3068?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) Report
Attention: Patch coverage is `76.08696%` with `33 lines` in your changes missing coverage. Please review.
> Project coverage is 51.21%. Comparing base [(`faa838f`)](https://app.codecov.io/gh/akuity/kargo/commit/faa838f12aeb32f32dea7d808141069df42dc7d3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) to head [(`043d3c0`)](https://app.codecov.io/gh/akuity/kargo/commit/043d3c0bd878602720caf31544e4c1889489731b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).

| [Files with missing lines](https://app.codecov.io/gh/akuity/kargo/pull/3068?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) | Patch % | Lines |
|---|---|---|
| [internal/controller/promotions/promotions.go](https://app.codecov.io/gh/akuity/kargo/pull/3068?src=pr&el=tree&filepath=internal%2Fcontroller%2Fpromotions%2Fpromotions.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-aW50ZXJuYWwvY29udHJvbGxlci9wcm9tb3Rpb25zL3Byb21vdGlvbnMuZ28=) | 0.00% | [20 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/3068?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |
| [internal/directives/simple\_engine\_promote.go](https://app.codecov.io/gh/akuity/kargo/pull/3068?src=pr&el=tree&filepath=internal%2Fdirectives%2Fsimple_engine_promote.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-aW50ZXJuYWwvZGlyZWN0aXZlcy9zaW1wbGVfZW5naW5lX3Byb21vdGUuZ28=) | 92.55% | [6 Missing and 1 partial :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/3068?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |
| [internal/directives/argocd\_updater.go](https://app.codecov.io/gh/akuity/kargo/pull/3068?src=pr&el=tree&filepath=internal%2Fdirectives%2Fargocd_updater.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-aW50ZXJuYWwvZGlyZWN0aXZlcy9hcmdvY2RfdXBkYXRlci5nbw==) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/3068?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |
| [api/v1alpha1/promotion\_types.go](https://app.codecov.io/gh/akuity/kargo/pull/3068?src=pr&el=tree&filepath=api%2Fv1alpha1%2Fpromotion_types.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-YXBpL3YxYWxwaGExL3Byb21vdGlvbl90eXBlcy5nbw==) | 80.00% | [2 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/3068?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff               @@
##           release-1.1    #3068      +/-   ##
===============================================
+ Coverage        51.09%   51.21%   +0.11%     
===============================================
  Files              283      283              
  Lines            25406    25450      +44     
===============================================
+ Hits             12981    13033      +52     
+ Misses           11726    11720       -6     
+ Partials           699      697       -2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/akuity/kargo/pull/3068?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).


Patch:
diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go
index 9c65633d..fcc7d439 100644
--- a/api/v1alpha1/generated.pb.go
+++ b/api/v1alpha1/generated.pb.go
@@ -1430,10 +1430,38 @@ func (m *StageStatus) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_StageStatus proto.InternalMessageInfo
 
+func (m *StepExecutionMetadata) Reset()      { *m = StepExecutionMetadata{} }
+func (*StepExecutionMetadata) ProtoMessage() {}
+func (*StepExecutionMetadata) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e26b7f7bbc391025, []int{50}
+}
+func (m *StepExecutionMetadata) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *StepExecutionMetadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	b = b[:cap(b)]
+	n, err := m.MarshalToSizedBuffer(b)
+	if err != nil {
+		return nil, err
+	}
+	return b[:n], nil
+}
+func (m *StepExecutionMetadata) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_StepExecutionMetadata.Merge(m, src)
+}
+func (m *StepExecutionMetadata) XXX_Size() int {
+	return m.Size()
+}
+func (m *StepExecutionMetadata) XXX_DiscardUnknown() {
+	xxx_messageInfo_StepExecutionMetadata.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_StepExecutionMetadata proto.InternalMessageInfo
+
 func (m *Verification) Reset()      { *m = Verification{} }
 func (*Verification) ProtoMessage() {}
 func (*Verification) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{50}
+	return fileDescriptor_e26b7f7bbc391025, []int{51}
 }
 func (m *Verification) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1461,7 +1489,7 @@ var xxx_messageInfo_Verification proto.InternalMessageInfo
 func (m *VerificationInfo) Reset()      { *m = VerificationInfo{} }
 func (*VerificationInfo) ProtoMessage() {}
 func (*VerificationInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{51}
+	return fileDescriptor_e26b7f7bbc391025, []int{52}
 }
 func (m *VerificationInfo) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1489,7 +1517,7 @@ var xxx_messageInfo_VerificationInfo proto.InternalMessageInfo
 func (m *VerifiedStage) Reset()      { *m = VerifiedStage{} }
 func (*VerifiedStage) ProtoMessage() {}
 func (*VerifiedStage) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{52}
+	return fileDescriptor_e26b7f7bbc391025, []int{53}
 }
 func (m *VerifiedStage) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1517,7 +1545,7 @@ var xxx_messageInfo_VerifiedStage proto.InternalMessageInfo
 func (m *Warehouse) Reset()      { *m = Warehouse{} }
 func (*Warehouse) ProtoMessage() {}
 func (*Warehouse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{53}
+	return fileDescriptor_e26b7f7bbc391025, []int{54}
 }
 func (m *Warehouse) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1545,7 +1573,7 @@ var xxx_messageInfo_Warehouse proto.InternalMessageInfo
 func (m *WarehouseList) Reset()      { *m = WarehouseList{} }
 func (*WarehouseList) ProtoMessage() {}
 func (*WarehouseList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{54}
+	return fileDescriptor_e26b7f7bbc391025, []int{55}
 }
 func (m *WarehouseList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1573,7 +1601,7 @@ var xxx_messageInfo_WarehouseList proto.InternalMessageInfo
 func (m *WarehouseSpec) Reset()      { *m = WarehouseSpec{} }
 func (*WarehouseSpec) ProtoMessage() {}
 func (*WarehouseSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{55}
+	return fileDescriptor_e26b7f7bbc391025, []int{56}
 }
 func (m *WarehouseSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1601,7 +1629,7 @@ var xxx_messageInfo_WarehouseSpec proto.InternalMessageInfo
 func (m *WarehouseStatus) Reset()      { *m = WarehouseStatus{} }
 func (*WarehouseStatus) ProtoMessage() {}
 func (*WarehouseStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{56}
+	return fileDescriptor_e26b7f7bbc391025, []int{57}
 }
 func (m *WarehouseStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1682,6 +1710,7 @@ func init() {
 	proto.RegisterType((*StageList)(nil), "github.com.akuity.kargo.api.v1alpha1.StageList")
 	proto.RegisterType((*StageSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.StageSpec")
 	proto.RegisterType((*StageStatus)(nil), "github.com.akuity.kargo.api.v1alpha1.StageStatus")
+	proto.RegisterType((*StepExecutionMetadata)(nil), "github.com.akuity.kargo.api.v1alpha1.StepExecutionMetadata")
 	proto.RegisterType((*Verification)(nil), "github.com.akuity.kargo.api.v1alpha1.Verification")
 	proto.RegisterType((*VerificationInfo)(nil), "github.com.akuity.kargo.api.v1alpha1.VerificationInfo")
 	proto.RegisterType((*VerifiedStage)(nil), "github.com.akuity.kargo.api.v1alpha1.VerifiedStage")
@@ -1696,236 +1725,243 @@ func init() {
 }
 
 var fileDescriptor_e26b7f7bbc391025 = []byte{
-	// 3654 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x3c, 0x4d, 0x6f, 0x23, 0x47,
-	0x76, 0xd3, 0xa4, 0x44, 0x89, 0x8f, 0xfa, 0xac, 0xd1, 0xd8, 0x5a, 0x6d, 0x2c, 0x4d, 0xda, 0x86,
-	0x61, 0xc7, 0x36, 0x95, 0xf9, 0xf2, 0x7c, 0x38, 0x99, 0x40, 0xa4, 0xe6, 0x43, 0x13, 0xad, 0x47,
-	0x29, 0x8e, 0x67, 0x76, 0x67, 0x67, 0xe0, 0x94, 0xc8, 0x12, 0xd9, 0x11, 0xd9, 0xcd, 0xed, 0x2a,
-	0x6a, 0xad, 0x24, 0x48, 0x36, 0x5f, 0xc0, 0x02, 0xf9, 0x40, 0x0e, 0x06, 0xbc, 0x01, 0x12, 0x20,
-	0x48, 0x8e, 0x41, 0xf2, 0x07, 0x72, 0xf0, 0x21, 0x17, 0x23, 0x70, 0x02, 0x23, 0xc9, 0xc1, 0x07,
-	0x43, 0x88, 0x65, 0x20, 0x47, 0xdf, 0x72, 0x19, 0x20, 0x40, 0x50, 0x1f, 0xdd, 0x5d, 0xdd, 0x6c,
-	0x8e, 0xd8, 0x1c, 0x69, 0x30, 0xd8, 0x1b, 0x55, 0xef, 0xab, 0xea, 0xd5, 0x7b, 0xaf, 0xde, 0x7b,
-	0x55, 0x2d, 0xb8, 0xd8, 0x74, 0x78, 0xab, 0xb7, 0x5d, 0xae, 0x7b, 0x9d, 0x55, 0xb2, 0xdb, 0x73,
-	0xf8, 0xfe, 0xea, 0x2e, 0xf1, 0x9b, 0xde, 0x2a, 0xe9, 0x3a, 0xab, 0x7b, 0xe7, 0x48, 0xbb, 0xdb,
-	0x22, 0xe7, 0x56, 0x9b, 0xd4, 0xa5, 0x3e, 0xe1, 0xb4, 0x51, 0xee, 0xfa, 0x1e, 0xf7, 0xd0, 0x6b,
-	0x11, 0x55, 0x59, 0x51, 0x95, 0x25, 0x55, 0x99, 0x74, 0x9d, 0x72, 0x40, 0xb5, 0xf4, 0x8e, 0xc1,
-	0xbb, 0xe9, 0x35, 0xbd, 0x55, 0x49, 0xbc, 0xdd, 0xdb, 0x91, 0x7f, 0xc9, 0x3f, 0xe4, 0x2f, 0xc5,
-	0x74, 0xe9, 0xf6, 0xee, 0x15, 0x56, 0x76, 0xa4, 0x64, 0xfa, 0x11, 0xa7, 0x2e, 0x73, 0x3c, 0x97,
-	0xbd, 0x43, 0xba, 0x0e, 0xa3, 0xfe, 0x1e, 0xf5, 0x57, 0xbb, 0xbb, 0x4d, 0x01, 0x63, 0x71, 0x84,
-	0xd5, 0xbd, 0xbe, 0xe9, 0x2d, 0x5d, 0x8c, 0x38, 0x75, 0x48, 0xbd, 0xe5, 0xb8, 0xd4, 0xdf, 0x8f,
-	0xc8, 0x3b, 0x94, 0x93, 0x34, 0xaa, 0xd5, 0x41, 0x54, 0x7e, 0xcf, 0xe5, 0x4e, 0x87, 0xf6, 0x11,
-	0xbc, 0x7b, 0x14, 0x01, 0xab, 0xb7, 0x68, 0x87, 0x24, 0xe9, 0xec, 0x47, 0x70, 0x7a, 0xcd, 0x25,
-	0xed, 0x7d, 0xe6, 0x30, 0xdc, 0x73, 0xd7, 0xfc, 0x66, 0xaf, 0x43, 0x5d, 0x8e, 0xce, 0xc2, 0x98,
-	0x4b, 0x3a, 0x74, 0xd1, 0x3a, 0x6b, 0xbd, 0x51, 0xac, 0x4c, 0x7d, 0x76, 0xb0, 0x72, 0xea, 0xf0,
-	0x60, 0x65, 0xec, 0x7d, 0xd2, 0xa1, 0x58, 0x42, 0xd0, 0xab, 0x30, 0xbe, 0x47, 0xda, 0x3d, 0xba,
-	0x98, 0x93, 0x28, 0xd3, 0x1a, 0x65, 0xfc, 0xbe, 0x18, 0xc4, 0x0a, 0x66, 0xff, 0x51, 0x3e, 0xc6,
-	0xfe, 0x7b, 0x94, 0x93, 0x06, 0xe1, 0x04, 0x75, 0xa0, 0xd0, 0x26, 0xdb, 0xb4, 0xcd, 0x16, 0xad,
-	0xb3, 0xf9, 0x37, 0x4a, 0xe7, 0x6f, 0x94, 0x87, 0xd9, 0xc4, 0x72, 0x0a, 0xab, 0xf2, 0xa6, 0xe4,
-	0x73, 0xc3, 0xe5, 0xfe, 0x7e, 0x65, 0x46, 0x4f, 0xa2, 0xa0, 0x06, 0xb1, 0x16, 0x82, 0xfe, 0xc0,
-	0x82, 0x12, 0x71, 0x5d, 0x8f, 0x13, 0x2e, 0xb6, 0x69, 0x31, 0x27, 0x85, 0xde, 0x19, 0x5d, 0xe8,
-	0x5a, 0xc4, 0x4c, 0x49, 0x3e, 0xad, 0x25, 0x97, 0x0c, 0x08, 0x36, 0x65, 0x2e, 0x5d, 0x85, 0x92,
-	0x31, 0x55, 0x34, 0x07, 0xf9, 0x5d, 0xba, 0xaf, 0xf4, 0x8b, 0xc5, 0x4f, 0xb4, 0x10, 0x53, 0xa8,
-	0xd6, 0xe0, 0xb5, 0xdc, 0x15, 0x6b, 0xe9, 0x3a, 0xcc, 0x25, 0x05, 0x66, 0xa1, 0xb7, 0xff, 0xc2,
-	0x82, 0x05, 0x63, 0x15, 0x98, 0xee, 0x50, 0x9f, 0xba, 0x75, 0x8a, 0x56, 0xa1, 0x28, 0xf6, 0x92,
-	0x75, 0x49, 0x3d, 0xd8, 0xea, 0x79, 0xbd, 0x90, 0xe2, 0xfb, 0x01, 0x00, 0x47, 0x38, 0xa1, 0x59,
-	0xe4, 0x9e, 0x66, 0x16, 0xdd, 0x16, 0x61, 0x74, 0x31, 0x1f, 0x37, 0x8b, 0x2d, 0x31, 0x88, 0x15,
-	0xcc, 0xfe, 0x55, 0xf8, 0x4e, 0x30, 0x9f, 0x7b, 0xb4, 0xd3, 0x6d, 0x13, 0x4e, 0xa3, 0x49, 0x1d,
-	0x69, 0x7a, 0xf6, 0x2c, 0x4c, 0xaf, 0x75, 0xbb, 0xbe, 0xb7, 0x47, 0x1b, 0x35, 0x4e, 0x9a, 0xd4,
-	0xfe, 0x43, 0x0b, 0xce, 0xac, 0xf9, 0x4d, 0xaf, 0xba, 0xbe, 0xd6, 0xed, 0xde, 0xa6, 0xa4, 0xcd,
-	0x5b, 0x35, 0x4e, 0x78, 0x8f, 0xa1, 0xeb, 0x50, 0x60, 0xf2, 0x97, 0x66, 0xf7, 0x7a, 0x60, 0x21,
-	0x0a, 0xfe, 0xe4, 0x60, 0x65, 0x21, 0x85, 0x90, 0x62, 0x4d, 0x85, 0xde, 0x84, 0x89, 0x0e, 0x65,
-	0x8c, 0x34, 0x83, 0x35, 0xcf, 0x6a, 0x06, 0x13, 0xdf, 0x53, 0xc3, 0x38, 0x80, 0xdb, 0xff, 0x9a,
-	0x83, 0xd9, 0x90, 0x97, 0x16, 0x7f, 0x02, 0x0a, 0xee, 0xc1, 0x54, 0xcb, 0x58, 0xa1, 0xd4, 0x73,
-	0xe9, 0xfc, 0x7b, 0x43, 0xda, 0x72, 0x9a, 0x92, 0x2a, 0x0b, 0x5a, 0xcc, 0x94, 0x39, 0x8a, 0x63,
-	0x62, 0x50, 0x07, 0x80, 0xed, 0xbb, 0x75, 0x2d, 0x74, 0x4c, 0x0a, 0xbd, 0x9a, 0x51, 0x68, 0x2d,
-	0x64, 0x50, 0x41, 0x5a, 0x24, 0x44, 0x63, 0xd8, 0x10, 0x60, 0xff, 0x93, 0x05, 0xa7, 0x53, 0xe8,
-	0xd0, 0xaf, 0x24, 0xf6, 0xf3, 0xb5, 0xbe, 0xfd, 0x44, 0x7d, 0x64, 0xd1, 0x6e, 0xbe, 0x0d, 0x93,
-	0x3e, 0xdd, 0x73, 0x44, 0xac, 0xd6, 0x1a, 0x9e, 0xd3, 0xf4, 0x93, 0x58, 0x8f, 0xe3, 0x10, 0x03,
-	0xbd, 0x05, 0xc5, 0xe0, 0xb7, 0x50, 0x73, 0x5e, 0x98, 0xb3, 0xd8, 0xb8, 0x00, 0x95, 0xe1, 0x08,
-	0x6e, 0xff, 0x3e, 0x8c, 0x57, 0x5b, 0xc4, 0xe7, 0xc2, 0x62, 0x7c, 0xda, 0xf5, 0x3e, 0xc0, 0x9b,
-	0x7a, 0x8a, 0xa1, 0xc5, 0x60, 0x35, 0x8c, 0x03, 0xf8, 0x10, 0x9b, 0xfd, 0x26, 0x4c, 0xec, 0x51,
-	0x5f, 0xce, 0x37, 0x1f, 0x67, 0x76, 0x5f, 0x0d, 0xe3, 0x00, 0x6e, 0xff, 0xa7, 0x05, 0x0b, 0x72,
-	0x06, 0xeb, 0x0e, 0xab, 0x7b, 0x7b, 0xd4, 0xdf, 0xc7, 0x94, 0xf5, 0xda, 0xc7, 0x3c, 0xa1, 0x75,
-	0x98, 0x63, 0xb4, 0xb3, 0x47, 0xfd, 0xaa, 0xe7, 0x32, 0xee, 0x13, 0xc7, 0xe5, 0x7a, 0x66, 0x8b,
-	0x1a, 0x7b, 0xae, 0x96, 0x80, 0xe3, 0x3e, 0x0a, 0xf4, 0x06, 0x4c, 0xea, 0x69, 0x0b, 0x53, 0x12,
-	0x8a, 0x9d, 0x12, 0x7b, 0xa0, 0xd7, 0xc4, 0x70, 0x08, 0xb5, 0xff, 0xc7, 0x82, 0x79, 0xb9, 0xaa,
-	0x5a, 0x6f, 0x9b, 0xd5, 0x7d, 0xa7, 0x2b, 0x42, 0xe0, 0x8b, 0xb8, 0xa4, 0xeb, 0x30, 0xd3, 0x08,
-	0x14, 0xbf, 0xe9, 0x74, 0x1c, 0x2e, 0x7d, 0x64, 0xbc, 0xf2, 0x92, 0xe6, 0x31, 0xb3, 0x1e, 0x83,
-	0xe2, 0x04, 0xb6, 0xfd, 0xd7, 0x79, 0x38, 0x1d, 0xa0, 0xd0, 0xc6, 0x9a, 0xcf, 0x9d, 0x1d, 0x52,
-	0xe7, 0x0c, 0x35, 0x60, 0xaa, 0x11, 0x0d, 0x73, 0xed, 0x79, 0xbf, 0x54, 0x56, 0xc7, 0x7d, 0xd9,
-	0x3c, 0xee, 0xcb, 0xdd, 0xdd, 0xa6, 0x18, 0x60, 0x65, 0x91, 0x55, 0x94, 0xf7, 0xce, 0x95, 0xef,
-	0x39, 0x1d, 0x1a, 0x79, 0xb7, 0xc1, 0x9e, 0xe3, 0x18, 0x57, 0xf4, 0x00, 0xf2, 0x4d, 0x87, 0xeb,
-	0xc3, 0xf8, 0xca, 0x70, 0x6e, 0x7d, 0xcb, 0x49, 0x9a, 0x5a, 0xa5, 0xa4, 0x45, 0xe5, 0x6f, 0x39,
-	0x1c, 0x0b, 0x8e, 0x68, 0x1b, 0x0a, 0x4e, 0x87, 0x34, 0x69, 0x70, 0xe6, 0x5e, 0x1b, 0x8e, 0xf7,
-	0x86, 0xa0, 0x49, 0x72, 0x0f, 0x4f, 0x77, 0x09, 0x65, 0x58, 0x73, 0x16, 0x32, 0xea, 0xc2, 0x44,
-	0x94, 0x93, 0x0e, 0x2d, 0x23, 0xcd, 0x59, 0x22, 0x19, 0x12, 0xca, 0xb0, 0xe6, 0x6c, 0x7f, 0x99,
-	0x83, 0xb9, 0x48, 0x7f, 0x55, 0xaf, 0xd3, 0x71, 0x38, 0x5a, 0x82, 0x9c, 0xd3, 0xd0, 0x16, 0x08,
-	0x9a, 0x30, 0xb7, 0xb1, 0x8e, 0x73, 0x4e, 0x03, 0xbd, 0x0e, 0x85, 0x6d, 0x9f, 0xb8, 0xf5, 0x96,
-	0xb6, 0xbc, 0x90, 0x71, 0x45, 0x8e, 0x62, 0x0d, 0x45, 0xaf, 0x40, 0x9e, 0x93, 0xa6, 0x36, 0xb8,
-	0x50, 0x7f, 0xf7, 0x48, 0x13, 0x8b, 0x71, 0x61, 0xe9, 0xac, 0xb7, 0xfd, 0x5b, 0xb4, 0xae, 0x76,
-	0xde, 0xb0, 0xf4, 0x9a, 0x1a, 0xc6, 0x01, 0x5c, 0x48, 0x24, 0x3d, 0xde, 0xf2, 0xfc, 0xc5, 0xf1,
-	0xb8, 0xc4, 0x35, 0x39, 0x8a, 0x35, 0x54, 0x9c, 0x49, 0x75, 0x39, 0x7f, 0x4e, 0xfd, 0xc5, 0x42,
-	0xfc, 0x4c, 0xaa, 0x06, 0x00, 0x1c, 0xe1, 0xa0, 0xc7, 0x50, 0xaa, 0xfb, 0x94, 0x70, 0xcf, 0x5f,
-	0x27, 0x9c, 0x2e, 0x4e, 0x64, 0xb6, 0xc0, 0x59, 0x91, 0x18, 0x55, 0x23, 0x16, 0xd8, 0xe4, 0x67,
-	0x7f, 0x6b, 0xc1, 0x62, 0xa4, 0x5a, 0xb9, 0xb7, 0x51, 0x32, 0xa0, 0xd5, 0x63, 0x0d, 0x50, 0xcf,
-	0xeb, 0x50, 0x68, 0x38, 0x4d, 0xca, 0x78, 0x52, 0xcb, 0xeb, 0x72, 0x14, 0x6b, 0x28, 0x3a, 0x0f,
-	0xd0, 0x74, 0xb8, 0x0e, 0x0e, 0x5a, 0xd9, 0xe1, 0x11, 0x74, 0x2b, 0x84, 0x60, 0x03, 0x0b, 0x3d,
-	0x80, 0xa2, 0x9c, 0xe6, 0x88, 0x6e, 0x27, 0x8f, 0x8a, 0x6a, 0xc0, 0x00, 0x47, 0xbc, 0xec, 0x2f,
-	0xc6, 0x60, 0xe2, 0xa6, 0x4f, 0x9d, 0x66, 0x8b, 0xa3, 0xdf, 0x84, 0xc9, 0x8e, 0x4e, 0x2a, 0xe5,
-	0x22, 0x4b, 0xe7, 0x7f, 0x79, 0x38, 0x19, 0x77, 0xe5, 0xa6, 0x8b, 0x84, 0x34, 0x5a, 0x48, 0x34,
-	0x86, 0x43, 0xae, 0x22, 0x21, 0x23, 0x6d, 0x87, 0x30, 0xb9, 0x6f, 0x46, 0x42, 0xb6, 0x26, 0x06,
-	0xb1, 0x82, 0xa1, 0x1f, 0x42, 0xc1, 0xf3, 0x9d, 0xa6, 0xe3, 0x2e, 0x16, 0xe5, 0x24, 0x2e, 0x0c,
-	0xe7, 0x42, 0x7a, 0x15, 0x77, 0x25, 0x69, 0xa4, 0x7c, 0xf5, 0x37, 0xd6, 0x2c, 0xd1, 0x43, 0x98,
-	0x50, 0xc6, 0x14, 0x38, 0xe8, 0xea, 0xd0, 0x01, 0x46, 0xd9, 0x63, 0x64, 0xf4, 0xea, 0x6f, 0x86,
-	0x03, 0x86, 0xa8, 0x16, 0xc6, 0x97, 0x31, 0xc9, 0xfa, 0xad, 0x0c, 0xf1, 0x65, 0x60, 0x40, 0xa9,
-	0x85, 0x01, 0x65, 0x3c, 0x0b, 0x53, 0x19, 0x32, 0x06, 0x45, 0x10, 0xa1, 0x62, 0x9d, 0xb9, 0x14,
-	0x46, 0x50, 0xb1, 0x4e, 0x9b, 0x66, 0xe2, 0xe9, 0x4e, 0x90, 0xd8, 0xd8, 0x1f, 0xe7, 0x61, 0x5e,
-	0x63, 0x56, 0xbd, 0x76, 0x9b, 0xd6, 0xe5, 0x31, 0xa9, 0xe2, 0x53, 0x3e, 0x35, 0x3e, 0x39, 0x30,
-	0xee, 0x70, 0xda, 0x09, 0x0a, 0xb0, 0x4a, 0xa6, 0xd9, 0x44, 0x32, 0xca, 0x1b, 0x82, 0x89, 0xaa,
-	0x81, 0xc2, 0x5d, 0xd2, 0x58, 0x58, 0x49, 0x40, 0x7f, 0x62, 0xc1, 0xe9, 0x3d, 0xea, 0x3b, 0x3b,
-	0x4e, 0x5d, 0x56, 0x30, 0xb7, 0x1d, 0xc6, 0x3d, 0x7f, 0x5f, 0x9f, 0x08, 0xef, 0x0e, 0x27, 0xf9,
-	0xbe, 0xc1, 0x60, 0xc3, 0xdd, 0xf1, 0x2a, 0xdf, 0xd5, 0xd2, 0x4e, 0xdf, 0xef, 0x67, 0x8d, 0xd3,
-	0xe4, 0x2d, 0x75, 0x01, 0xa2, 0xd9, 0xa6, 0x14, 0x50, 0x9b, 0x66, 0x01, 0x35, 0xf4, 0xc4, 0x82,
-	0xc5, 0x06, 0x21, 0xcb, 0x2c, 0xbc, 0x3e, 0xb5, 0xa0, 0xa4, 0xe1, 0x9b, 0x0e, 0xe3, 0xe8, 0x51,
-	0x9f, 0xb7, 0x97, 0x87, 0xf3, 0x76, 0x41, 0x2d, 0x7d, 0x3d, 0xcc, 0x57, 0x83, 0x11, 0xc3, 0xd3,
-	0x71, 0xb0, 0xa5, 0x4a, 0xb1, 0xef, 0x64, 0x9a, 0x7f, 0x14, 0x18, 0xa4, 0x8e, 0xf4, 0xde, 0xd9,
-	0x3e, 0x4c, 0xc7, 0x9c, 0x1c, 0x5d, 0x82, 0xb1, 0x5d, 0xc7, 0x0d, 0x4e, 0xbd, 0x5f, 0x0c, 0xf2,
-	0xa9, 0x5f, 0x77, 0xdc, 0xc6, 0x93, 0x83, 0x95, 0xf9, 0x18, 0xb2, 0x18, 0xc4, 0x12, 0xfd, 0xe8,
-	0x34, 0xec, 0xda, 0xe4, 0xcf, 0xfe, 0x76, 0xe5, 0xd4, 0x4f, 0xbe, 0x3a, 0x7b, 0xca, 0xfe, 0x24,
-	0x0f, 0x73, 0x49, 0xad, 0x0e, 0xd1, 0x90, 0x88, 0x62, 0xd8, 0xe4, 0x89, 0xc6, 0xb0, 0xdc, 0xc9,
-	0xc5, 0xb0, 0xfc, 0x49, 0xc4, 0xb0, 0xb1, 0x63, 0x8b, 0x61, 0xf6, 0xbf, 0x5b, 0x30, 0x13, 0xee,
-	0xcc, 0x8f, 0x7a, 0xe2, 0x64, 0x8d, 0xb4, 0x6e, 0x1d, 0xbf, 0xd6, 0x3f, 0x84, 0x09, 0xe6, 0xf5,
-	0xfc, 0xba, 0x4c, 0x1f, 0x05, 0xf7, 0x8b, 0xd9, 0x82, 0xa6, 0xa2, 0x35, 0x72, 0x26, 0x35, 0x80,
-	0x03, 0xae, 0xf6, 0xa3, 0x70, 0x3d, 0x1a, 0xa4, 0x32, 0x0a, 0x5f, 0xe4, 0x5b, 0x62, 0x3d, 0x93,
-	0x66, 0x46, 0x21, 0x46, 0xb1, 0x86, 0x22, 0x5b, 0x86, 0xf3, 0x20, 0xb1, 0x2d, 0x56, 0x40, 0x47,
-	0x65, 0xb9, 0x07, 0x0a, 0x62, 0x7f, 0x9b, 0x0f, 0xbd, 0x47, 0x97, 0xaf, 0x3f, 0x06, 0x50, 0x91,
-	0x89, 0x36, 0x36, 0x5c, 0x1d, 0x7a, 0xab, 0x23, 0x1c, 0x04, 0x3a, 0x1c, 0x0a, 0x2e, 0x2a, 0xf6,
-	0x86, 0x39, 0x40, 0x04, 0xc0, 0x86, 0x28, 0xf4, 0x3b, 0x50, 0x22, 0xba, 0x65, 0x72, 0xd3, 0xf3,
-	0xb5, 0x0d, 0xaf, 0x8f, 0x22, 0x79, 0x2d, 0x62, 0x93, 0x6c, 0x7d, 0x45, 0x10, 0x6c, 0x4a, 0x5b,
-	0xf2, 0x61, 0x36, 0x31, 0xdf, 0x94, 0xe8, 0xbb, 0x11, 0x8f, 0xbe, 0x17, 0xb2, 0x1c, 0x0b, 0xba,
-	0x0f, 0x64, 0xf6, 0xcc, 0x18, 0xcc, 0x25, 0x67, 0x7a, 0x6c, 0x42, 0x63, 0xcd, 0x27, 0x33, 0xde,
-	0xff, 0x4d, 0x0e, 0x8a, 0xa1, 0xc7, 0x67, 0xa9, 0x52, 0xd5, 0x49, 0x9d, 0x3b, 0xa2, 0x92, 0xc8,
-	0x0f, 0x53, 0x49, 0x8c, 0x0d, 0xae, 0x24, 0x82, 0x4e, 0x56, 0xe1, 0xe9, 0x9d, 0x2c, 0xa3, 0x92,
-	0x98, 0x18, 0xbe, 0x92, 0x98, 0x3c, 0xba, 0x92, 0xb0, 0xff, 0xce, 0x02, 0xd4, 0x5f, 0x36, 0x66,
-	0x51, 0x14, 0x49, 0xc6, 0xe1, 0x21, 0x4f, 0xe9, 0x64, 0xed, 0x36, 0x38, 0x1c, 0xdb, 0x9f, 0x8e,
-	0xc3, 0xec, 0x2d, 0x67, 0xe4, 0x86, 0x03, 0x87, 0x97, 0x15, 0xa7, 0x1a, 0xd5, 0x39, 0x52, 0x8d,
-	0xfb, 0x84, 0xd3, 0xe6, 0xbe, 0xde, 0xdf, 0x6b, 0x9a, 0xf4, 0xe5, 0x6a, 0x3a, 0xda, 0x93, 0xc1,
-	0x20, 0x3c, 0x88, 0xf5, 0xd0, 0x46, 0xf2, 0x1e, 0x4c, 0x33, 0xee, 0x3b, 0x75, 0xae, 0x5a, 0x1a,
-	0x6c, 0xb1, 0x24, 0xa3, 0xdc, 0x19, 0x8d, 0x3e, 0x5d, 0x33, 0x81, 0x38, 0x8e, 0x9b, 0xda, 0x29,
-	0x19, 0xcb, 0xdc, 0x29, 0x59, 0x85, 0x22, 0x69, 0xb7, 0xbd, 0x1f, 0xdf, 0x23, 0x4d, 0xa6, 0x4b,
-	0xd5, 0xd0, 0x6a, 0xd6, 0x02, 0x00, 0x8e, 0x70, 0x50, 0x19, 0xc0, 0x69, 0xba, 0x9e, 0x4f, 0x25,
-	0x45, 0x41, 0x86, 0xdb, 0x19, 0x11, 0xeb, 0x36, 0xc2, 0x51, 0x6c, 0x60, 0xa0, 0x1a, 0x9c, 0x71,
-	0x5c, 0x46, 0xeb, 0x3d, 0x9f, 0xd6, 0x76, 0x9d, 0xee, 0xbd, 0xcd, 0x9a, 0x8c, 0x12, 0xfb, 0xd2,
-	0x9a, 0x27, 0x2b, 0xaf, 0x68, 0x61, 0x67, 0x36, 0xd2, 0x90, 0x70, 0x3a, 0x2d, 0xba, 0x08, 0x53,
-	0x8e, 0x5b, 0x6f, 0xf7, 0x1a, 0x74, 0x8b, 0xf0, 0x16, 0x5b, 0x9c, 0x94, 0xd3, 0x98, 0x3b, 0x3c,
-	0x58, 0x99, 0xda, 0x30, 0xc6, 0x71, 0x0c, 0x4b, 0x50, 0xd1, 0x8f, 0x0c, 0xaa, 0x62, 0x44, 0x75,
-	0xe3, 0x23, 0x93, 0xca, 0xc4, 0x4a, 0xe9, 0x25, 0x41, 0xa6, 0x5e, 0xd2, 0x3f, 0xe4, 0xa0, 0xa0,
-	0x5a, 0xb9, 0xe8, 0x52, 0xa2, 0x5f, 0xfa, 0x4a, 0x5f, 0xbf, 0xb4, 0x94, 0xd6, 0xf6, 0xb6, 0xa1,
-	0xe0, 0x30, 0xd6, 0x8b, 0x9f, 0x6e, 0x1b, 0x72, 0x04, 0x6b, 0x88, 0x6c, 0xbb, 0x78, 0xee, 0x8e,
-	0xd3, 0xd4, 0xc5, 0xf1, 0x75, 0x23, 0x95, 0x8d, 0xae, 0xc4, 0x3e, 0x0c, 0xef, 0xcc, 0xa2, 0xac,
-	0x36, 0x86, 0x20, 0xd2, 0xdb, 0x3b, 0xb5, 0xbb, 0xef, 0x2b, 0x19, 0x55, 0xc9, 0x11, 0x6b, 0xce,
-	0x42, 0x86, 0xd7, 0xe3, 0xdd, 0x1e, 0x97, 0x86, 0x72, 0x4c, 0x32, 0xee, 0x4a, 0x8e, 0x58, 0x73,
-	0xb6, 0x3f, 0xb1, 0x60, 0x56, 0xe9, 0xa0, 0xda, 0xa2, 0xf5, 0xdd, 0x1a, 0xa7, 0x5d, 0x91, 0x6d,
-	0xf6, 0x18, 0x65, 0xc9, 0x6c, 0xf3, 0x03, 0x46, 0x19, 0x96, 0x10, 0x63, 0xf5, 0xb9, 0x93, 0x5a,
-	0xbd, 0xfd, 0x8f, 0x16, 0x8c, 0xcb, 0xb4, 0x2e, 0x4b, 0xfc, 0x89, 0xb7, 0x3a, 0x72, 0x43, 0xb5,
-	0x3a, 0x8e, 0x68, 0x42, 0x45, 0x5d, 0x96, 0xb1, 0xa7, 0x75, 0x59, 0xec, 0x6f, 0x2c, 0x58, 0x48,
-	0xeb, 0xdc, 0x65, 0x99, 0xfe, 0xdb, 0x30, 0xd9, 0x6d, 0x13, 0xbe, 0xe3, 0xf9, 0x9d, 0x64, 0x8b,
-	0x7e, 0x4b, 0x8f, 0xe3, 0x10, 0x03, 0xf9, 0x00, 0x7e, 0x50, 0x22, 0x04, 0xe9, 0xf3, 0xf5, 0xac,
-	0x27, 0x42, 0xbc, 0xe5, 0x14, 0x29, 0x2b, 0x1c, 0x62, 0xd8, 0x90, 0x62, 0xff, 0xe9, 0x38, 0xcc,
-	0x4b, 0x92, 0x51, 0x4f, 0x88, 0x51, 0x76, 0xa8, 0x0b, 0x2f, 0xc9, 0xc4, 0xbe, 0xff, 0x50, 0x51,
-	0x9b, 0x76, 0x45, 0xd3, 0xbf, 0xb4, 0x91, 0x8a, 0xf5, 0x64, 0x20, 0x04, 0x0f, 0xe0, 0xdb, 0x7f,
-	0x52, 0xc0, 0xcf, 0xdf, 0x49, 0x61, 0x1a, 0xdb, 0xc4, 0x91, 0xc6, 0x36, 0xf0, 0x5c, 0x99, 0x7c,
-	0x86, 0x73, 0xa5, 0x3f, 0xd6, 0x17, 0x33, 0xc5, 0xfa, 0xbf, 0xca, 0xc1, 0xc4, 0x96, 0xef, 0xc9,
-	0x0e, 0xf0, 0xc9, 0x37, 0x13, 0xef, 0xc2, 0x18, 0xeb, 0xd2, 0xba, 0x8e, 0x79, 0xe7, 0x86, 0xf3,
-	0x34, 0x3d, 0xbd, 0x5a, 0x97, 0xd6, 0x2b, 0x93, 0x22, 0x8c, 0x8a, 0x5f, 0x58, 0x32, 0x32, 0xba,
-	0x62, 0xf9, 0x2c, 0x19, 0x78, 0xc0, 0xf2, 0xe9, 0x5d, 0xb1, 0x4f, 0x2d, 0x28, 0x69, 0xcc, 0x17,
-	0xb6, 0xfd, 0xa2, 0xe7, 0x37, 0xa0, 0xfd, 0xf2, 0xe7, 0xd1, 0x0a, 0x84, 0xd2, 0xd0, 0xef, 0xc1,
-	0x7c, 0xd7, 0xf7, 0x3a, 0x9e, 0xf0, 0xd4, 0x2d, 0xaf, 0xed, 0xd4, 0x1d, 0x1a, 0x74, 0xf0, 0x2e,
-	0x0d, 0x2d, 0xcf, 0x20, 0xdf, 0xaf, 0x7c, 0x47, 0xcb, 0x9d, 0xdf, 0x4a, 0xf2, 0xc5, 0xfd, 0xa2,
-	0xec, 0xff, 0xb2, 0x60, 0x3a, 0xa6, 0x7b, 0x54, 0x07, 0xa8, 0x7b, 0x6e, 0xc3, 0xe1, 0xe1, 0x2d,
-	0x69, 0xe9, 0xfc, 0xea, 0x70, 0x5a, 0xad, 0x06, 0x74, 0x91, 0xd1, 0x85, 0x43, 0x0c, 0x1b, 0x6c,
-	0xd1, 0x85, 0xe0, 0x51, 0x41, 0x3c, 0x89, 0x51, 0x8f, 0x0a, 0x9e, 0x1c, 0xac, 0x4c, 0xe9, 0x39,
-	0x99, 0x8f, 0x0c, 0xb2, 0x5c, 0xdd, 0xff, 0x7d, 0x0e, 0x8a, 0xe1, 0xfa, 0x9f, 0x83, 0x1b, 0x7d,
-	0x10, 0x73, 0xa3, 0x0b, 0x19, 0x77, 0x4e, 0x3a, 0x52, 0x98, 0x93, 0x18, 0xce, 0xf4, 0x38, 0xe1,
-	0x4c, 0x59, 0x4d, 0xe2, 0x08, 0x77, 0xfa, 0x17, 0xb5, 0xf9, 0x0a, 0xf7, 0x39, 0x38, 0xd4, 0xbd,
-	0xb8, 0x43, 0xad, 0x66, 0x5c, 0xcd, 0x00, 0x97, 0xfa, 0xa9, 0x05, 0xb3, 0x09, 0x27, 0x40, 0xaf,
-	0xc2, 0xb8, 0x6c, 0xd9, 0x68, 0xfb, 0x0a, 0x09, 0x75, 0x81, 0x2f, 0x61, 0x68, 0x0b, 0x16, 0x48,
-	0x8f, 0x7b, 0x21, 0xed, 0x0d, 0x97, 0x6c, 0xb7, 0xa9, 0xaa, 0xda, 0x27, 0x2b, 0xbf, 0xa0, 0x69,
-	0x16, 0xd6, 0x52, 0x70, 0x70, 0x2a, 0xa5, 0xfd, 0x79, 0x0e, 0x50, 0x38, 0x98, 0xa5, 0xd5, 0xf9,
-	0x18, 0x26, 0x76, 0x54, 0x53, 0xe6, 0xd9, 0x7a, 0xd5, 0x95, 0x92, 0xd9, 0xae, 0x0f, 0x78, 0xa2,
-	0x1f, 0x1c, 0x8f, 0x1d, 0x41, 0xbf, 0x0d, 0xa1, 0x87, 0x00, 0x3b, 0x8e, 0xeb, 0xb0, 0xd6, 0x88,
-	0xb7, 0x6a, 0xf2, 0x34, 0xbf, 0x19, 0x72, 0xc0, 0x06, 0x37, 0xfb, 0xe3, 0x9c, 0x61, 0x9f, 0x32,
-	0x5c, 0x0e, 0xb5, 0xaf, 0x6f, 0xc6, 0x95, 0x59, 0xec, 0xbf, 0xc7, 0x30, 0x14, 0x33, 0xb6, 0x47,
-	0xfc, 0xa0, 0xa5, 0x7a, 0x39, 0xa3, 0x5a, 0xee, 0x13, 0xdf, 0x11, 0x1b, 0x1f, 0x6d, 0xe9, 0x7d,
-	0xe2, 0x33, 0x2c, 0x59, 0xa2, 0xef, 0x8b, 0xa9, 0xd2, 0x6e, 0x10, 0x42, 0x33, 0xc7, 0x04, 0x4e,
-	0xbb, 0xe6, 0xfa, 0x68, 0x97, 0x61, 0xc5, 0xd0, 0xfe, 0xb7, 0x82, 0x61, 0xf0, 0x3a, 0x6a, 0xdf,
-	0x01, 0xd4, 0x26, 0x8c, 0xdf, 0x26, 0x6e, 0x43, 0x98, 0x27, 0xdd, 0xf1, 0x29, 0x6b, 0xe9, 0x0c,
-	0x6d, 0x49, 0x73, 0x41, 0x9b, 0x7d, 0x18, 0x38, 0x85, 0x0a, 0x5d, 0x8a, 0x07, 0xe7, 0x95, 0x64,
-	0x70, 0x9e, 0x89, 0xbc, 0x6d, 0xb4, 0xf0, 0x6c, 0x9a, 0xfb, 0xf8, 0x09, 0x98, 0xfb, 0xef, 0xc2,
-	0xfc, 0x4e, 0xf2, 0x5e, 0x4b, 0xdf, 0x72, 0x5f, 0x1e, 0xf1, 0x5a, 0xac, 0x72, 0xe6, 0x30, 0xba,
-	0x0c, 0x89, 0x86, 0x71, 0xbf, 0x20, 0xe4, 0x05, 0xef, 0xb9, 0x64, 0xf5, 0xa9, 0x1a, 0x0b, 0x43,
-	0xbb, 0x5c, 0xa2, 0x6e, 0x4d, 0xbe, 0xe4, 0x52, 0x2c, 0x71, 0x4c, 0x40, 0xc2, 0x05, 0x0b, 0xc7,
-	0xe9, 0x82, 0xe8, 0x12, 0x94, 0xea, 0x3d, 0xdf, 0xa7, 0x2e, 0x17, 0xd3, 0x91, 0xa9, 0x6c, 0x3e,
-	0x6a, 0x10, 0x57, 0x23, 0x10, 0x36, 0xf1, 0x84, 0x39, 0x1a, 0x7f, 0xae, 0x71, 0x4e, 0x3b, 0x5d,
-	0x2e, 0x5b, 0x53, 0xf9, 0xc8, 0x1c, 0xab, 0x7d, 0x18, 0x38, 0x85, 0x0a, 0x7d, 0x28, 0x7d, 0x9e,
-	0x53, 0x59, 0xaf, 0x3c, 0x7b, 0x5d, 0x5e, 0xd4, 0xf1, 0x82, 0xab, 0x78, 0xc1, 0xa9, 0xfd, 0x67,
-	0xb1, 0x30, 0x33, 0x5c, 0xb7, 0x60, 0x09, 0x72, 0x84, 0x25, 0xfb, 0xbb, 0x6b, 0x0c, 0xe7, 0x08,
-	0x43, 0x3f, 0x80, 0x71, 0x9f, 0x72, 0x7f, 0x5f, 0x47, 0xc3, 0x2b, 0x23, 0x78, 0x3e, 0x16, 0xf4,
-	0x6a, 0xaa, 0xf2, 0x27, 0x56, 0x1c, 0x8d, 0x26, 0x45, 0xfe, 0xc4, 0x9a, 0x14, 0x15, 0xe3, 0x0c,
-	0x0b, 0xe7, 0x22, 0x2a, 0x2b, 0xa2, 0x36, 0x44, 0xa9, 0x25, 0x1f, 0x9d, 0xf4, 0x7a, 0xa3, 0x18,
-	0x0e, 0x31, 0x6c, 0x1f, 0xa2, 0xf4, 0x33, 0x78, 0x10, 0x8a, 0x1e, 0xeb, 0x24, 0xc9, 0xca, 0xf2,
-	0xc0, 0xb1, 0x8f, 0xcd, 0xa0, 0x64, 0xc9, 0xfe, 0xdc, 0x82, 0x33, 0xa9, 0xd8, 0x61, 0x94, 0xcf,
-	0x9d, 0x60, 0x94, 0xb7, 0x8e, 0x3b, 0xca, 0x3f, 0x34, 0x54, 0x18, 0x4c, 0xe1, 0xb8, 0x5e, 0x71,
-	0xff, 0x2c, 0x07, 0x73, 0x98, 0x76, 0xbd, 0x58, 0xc3, 0x63, 0x2b, 0x78, 0x32, 0x96, 0x21, 0x43,
-	0x48, 0xb4, 0xd5, 0x2b, 0x13, 0xb1, 0xb7, 0x62, 0xdf, 0x87, 0x71, 0xd9, 0x8b, 0xd0, 0x39, 0xcd,
-	0xe5, 0x0c, 0xd7, 0xa0, 0x31, 0xae, 0xd2, 0x0f, 0x54, 0x53, 0x47, 0x31, 0x14, 0x9c, 0xe5, 0x0d,
-	0xa6, 0x76, 0x83, 0xcb, 0x19, 0xee, 0x42, 0xfb, 0x39, 0xcb, 0x61, 0xac, 0x18, 0xda, 0x9f, 0xe4,
-	0x40, 0x65, 0x13, 0xcf, 0xa1, 0x6a, 0xf8, 0x8d, 0x58, 0xd5, 0x30, 0x64, 0x3a, 0x2c, 0x27, 0x37,
-	0xb0, 0x62, 0x48, 0x66, 0x7a, 0xe7, 0xb2, 0x30, 0x7d, 0x7a, 0xb5, 0xf0, 0xcf, 0x16, 0x14, 0x25,
-	0xde, 0x73, 0xa8, 0x14, 0xb6, 0xe2, 0x95, 0xc2, 0x5b, 0x19, 0x56, 0x31, 0xa0, 0x4a, 0xf8, 0x38,
-	0xaf, 0x67, 0x1f, 0xe6, 0x91, 0x2d, 0xe2, 0x37, 0x74, 0x86, 0x14, 0x79, 0xa0, 0x18, 0xc4, 0x0a,
-	0x86, 0x7e, 0x1b, 0xe6, 0x7c, 0x75, 0x29, 0x4e, 0x1b, 0x37, 0xc3, 0x74, 0x25, 0x9f, 0xf9, 0xd6,
-	0x5a, 0xdf, 0xac, 0x47, 0x9d, 0x32, 0x9c, 0xe0, 0x8a, 0xfb, 0xe4, 0x88, 0x14, 0xa6, 0x9b, 0x8c,
-	0x65, 0xfa, 0x68, 0xbf, 0x3c, 0x62, 0xe0, 0x54, 0x29, 0x4c, 0xdf, 0x30, 0xee, 0x17, 0x84, 0x5a,
-	0x30, 0x65, 0xbe, 0xb7, 0xd1, 0xb6, 0x74, 0x3e, 0xfb, 0xc3, 0x1e, 0x75, 0x33, 0x62, 0x8e, 0xe0,
-	0x18, 0x67, 0xfb, 0xa0, 0x00, 0x25, 0xc3, 0xf8, 0x12, 0xdd, 0x87, 0xe9, 0x93, 0xe9, 0x3e, 0xa4,
-	0x27, 0xcb, 0xa5, 0x91, 0x92, 0xe5, 0x73, 0xf1, 0x64, 0xf9, 0xbb, 0xc9, 0x64, 0x19, 0xe4, 0xea,
-	0x62, 0x89, 0x32, 0x83, 0x19, 0x9d, 0x35, 0x06, 0x0f, 0xa7, 0x32, 0x95, 0x1f, 0xfd, 0xb9, 0x29,
-	0x3a, 0x3c, 0x58, 0x09, 0x9e, 0x3d, 0x04, 0x0f, 0xa6, 0x12, 0x22, 0xd0, 0xf5, 0x50, 0x68, 0xad,
-	0xd7, 0xe9, 0x10, 0x7f, 0x7f, 0x71, 0x4a, 0x4e, 0x38, 0x6c, 0x4b, 0xde, 0x8c, 0x41, 0x71, 0x02,
-	0x1b, 0x6d, 0x41, 0x41, 0x25, 0x9d, 0xfa, 0x31, 0xce, 0xdb, 0x59, 0xf2, 0x59, 0x95, 0x67, 0xa8,
-	0xdf, 0x58, 0xf3, 0x31, 0xeb, 0x85, 0xe2, 0x11, 0xf5, 0xc2, 0x1d, 0x40, 0xde, 0xb6, 0xcc, 0x68,
-	0x1a, 0xb7, 0xd4, 0xe7, 0x4e, 0xc2, 0x2a, 0x0b, 0xf1, 0x74, 0xf2, 0x6e, 0x1f, 0x06, 0x4e, 0xa1,
-	0x12, 0x5e, 0xad, 0x93, 0xcc, 0xd0, 0x15, 0x74, 0x6d, 0x90, 0x35, 0x51, 0x8b, 0xca, 0x90, 0x05,
-	0xe1, 0xd5, 0xd5, 0x04, 0x57, 0xdc, 0x27, 0x07, 0xfd, 0x08, 0xa6, 0x85, 0x09, 0x45, 0x82, 0xe1,
-	0x19, 0x05, 0xcf, 0x1f, 0x1e, 0xac, 0x4c, 0x6f, 0x9a, 0x2c, 0x71, 0x5c, 0x82, 0xfd, 0x7f, 0x39,
-	0x88, 0xf9, 0x1f, 0xfa, 0xa9, 0x05, 0xf3, 0x24, 0xf1, 0xad, 0x4e, 0x90, 0xbe, 0xfc, 0x5a, 0xb6,
-	0x0f, 0xa8, 0xfa, 0x3e, 0xf5, 0x89, 0x9a, 0x8f, 0x49, 0x14, 0x86, 0xfb, 0x85, 0xa2, 0x3f, 0xb6,
-	0xe0, 0x34, 0xe9, 0xff, 0x18, 0x4b, 0x9f, 0x87, 0x57, 0x47, 0xfe, 0x9a, 0xab, 0xf2, 0xf2, 0xe1,
-	0xc1, 0x4a, 0xda, 0x67, 0x6a, 0x38, 0x4d, 0x1c, 0xfa, 0x21, 0x8c, 0x11, 0xbf, 0x19, 0x14, 0xea,
-	0xd9, 0xc5, 0x06, 0xdf, 0xd8, 0x45, 0x07, 0xf2, 0x9a, 0xdf, 0x64, 0x58, 0x32, 0xb5, 0xbf, 0xca,
-	0xc3, 0x5c, 0xf2, 0xa9, 0xa3, 0x7e, 0x1d, 0x32, 0x96, 0xfa, 0x3a, 0xe4, 0x55, 0x18, 0x27, 0x75,
-	0x1e, 0x3e, 0xd5, 0x88, 0x9e, 0xf7, 0x8a, 0x41, 0xac, 0x60, 0xe8, 0x01, 0x14, 0x19, 0x27, 0x3e,
-	0x17, 0xf5, 0x9b, 0x2e, 0xa1, 0x33, 0x3f, 0x65, 0xae, 0x05, 0x0c, 0x70, 0xc4, 0x0b, 0x5d, 0x89,
-	0x87, 0x33, 0x3b, 0x19, 0xce, 0xe6, 0xcd, 0xb5, 0x8c, 0x5a, 0xfe, 0x77, 0xa0, 0x64, 0xec, 0x83,
-	0x3e, 0x5d, 0xae, 0x65, 0xd6, 0x7b, 0x64, 0x76, 0xb3, 0xea, 0x43, 0xbd, 0x08, 0x62, 0xf2, 0x8f,
-	0xea, 0x63, 0xa9, 0xad, 0x67, 0xaa, 0x8f, 0xa5, 0xba, 0x0c, 0x6e, 0xf6, 0x2c, 0x4c, 0xc7, 0x5e,
-	0x2c, 0xc9, 0xce, 0xf3, 0x03, 0xe2, 0xd3, 0x96, 0xd7, 0x63, 0xf4, 0x45, 0xed, 0x3c, 0x87, 0x13,
-	0x3c, 0xee, 0xce, 0x73, 0xc4, 0xf8, 0xe8, 0xce, 0x73, 0x88, 0xfb, 0xc2, 0x76, 0x9e, 0xc3, 0x19,
-	0x0e, 0xc8, 0x29, 0xff, 0x37, 0x67, 0xac, 0x22, 0x9e, 0x57, 0xe6, 0x9e, 0x92, 0x57, 0x3e, 0x82,
-	0x49, 0xc7, 0xe5, 0xd4, 0xdf, 0x23, 0x6d, 0xdd, 0x22, 0x18, 0x72, 0xa9, 0xeb, 0x3d, 0x5f, 0x67,
-	0x55, 0xc1, 0x52, 0x37, 0x34, 0x1f, 0x1c, 0x72, 0x44, 0x6d, 0x38, 0x13, 0xf4, 0xa4, 0x7c, 0x4a,
-	0xa2, 0x9e, 0xb8, 0xbe, 0x57, 0x7e, 0x37, 0xb8, 0xd4, 0xbc, 0x99, 0x86, 0xf4, 0x64, 0x10, 0x00,
-	0xa7, 0x33, 0x45, 0x0c, 0xa6, 0x99, 0x51, 0x50, 0x05, 0x07, 0xc9, 0x90, 0xfd, 0xbc, 0x64, 0x0d,
-	0x6a, 0x5c, 0x46, 0x9b, 0x4c, 0x71, 0x5c, 0x86, 0xfd, 0x1f, 0x79, 0x98, 0x4d, 0x58, 0x5a, 0x22,
-	0x71, 0x2c, 0x3e, 0xcf, 0xc4, 0xb1, 0x30, 0x52, 0xe2, 0x98, 0x9e, 0xd3, 0x8c, 0x8d, 0x94, 0xd3,
-	0xbc, 0xa7, 0xf2, 0x0a, 0xbd, 0x73, 0x1b, 0xeb, 0xfa, 0xed, 0x5e, 0xa8, 0xcd, 0x4d, 0x13, 0x88,
-	0xe3, 0xb8, 0xf2, 0x14, 0x6e, 0xf4, 0x7f, 0xa8, 0xa6, 0x93, 0xa2, 0xab, 0x59, 0x1f, 0x5f, 0x84,
-	0x0c, 0xd4, 0x29, 0x9c, 0x02, 0xc0, 0x69, 0xe2, 0x2a, 0x77, 0x3e, 0xfb, 0x7a, 0xf9, 0xd4, 0x17,
-	0x5f, 0x2f, 0x9f, 0xfa, 0xf2, 0xeb, 0xe5, 0x53, 0x3f, 0x39, 0x5c, 0xb6, 0x3e, 0x3b, 0x5c, 0xb6,
-	0xbe, 0x38, 0x5c, 0xb6, 0xbe, 0x3c, 0x5c, 0xb6, 0xfe, 0xfb, 0x70, 0xd9, 0xfa, 0xcb, 0x6f, 0x96,
-	0x4f, 0x3d, 0x7c, 0x6d, 0x98, 0xff, 0x28, 0xf0, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x13, 0x1b,
-	0xe3, 0x9d, 0x78, 0x40, 0x00, 0x00,
+	// 3769 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5c, 0xcd, 0x6f, 0x1c, 0x47,
+	0x76, 0x57, 0xcf, 0x90, 0x43, 0xce, 0x1b, 0x52, 0x24, 0x4b, 0x94, 0xcd, 0xe5, 0xc6, 0xa4, 0xd2,
+	0x6b, 0x18, 0x76, 0x6c, 0x0f, 0x23, 0xd9, 0xb2, 0x64, 0x39, 0x51, 0xc0, 0x21, 0x25, 0x99, 0x0a,
+	0xd7, 0x62, 0x6a, 0x64, 0x79, 0xd7, 0x6b, 0xc3, 0x29, 0xce, 0x14, 0x67, 0x3a, 0x9c, 0xe9, 0x9e,
+	0xad, 0xaa, 0xe1, 0x9a, 0x49, 0x90, 0x6c, 0xbe, 0x80, 0x45, 0xbe, 0xb0, 0x07, 0x03, 0xde, 0x00,
+	0x09, 0x10, 0x24, 0xc7, 0x45, 0xf2, 0x0f, 0xe4, 0xe0, 0x43, 0x2e, 0x46, 0xb0, 0x08, 0x8c, 0x24,
+	0x07, 0x1f, 0x16, 0x44, 0xcc, 0x05, 0x72, 0xdc, 0x5b, 0x2e, 0x02, 0x02, 0x04, 0xf5, 0xd1, 0xdd,
+	0xd5, 0x3d, 0x3d, 0xe2, 0xf4, 0x88, 0x14, 0x84, 0xbd, 0x0d, 0xeb, 0x55, 0xfd, 0x5e, 0xd5, 0xab,
+	0xf7, 0x5e, 0xbd, 0xf7, 0xaa, 0x9a, 0xf0, 0x7a, 0xcb, 0x13, 0xed, 0xfe, 0x6e, 0xb5, 0x11, 0x74,
+	0xd7, 0xc8, 0x7e, 0xdf, 0x13, 0x87, 0x6b, 0xfb, 0x84, 0xb5, 0x82, 0x35, 0xd2, 0xf3, 0xd6, 0x0e,
+	0x2e, 0x93, 0x4e, 0xaf, 0x4d, 0x2e, 0xaf, 0xb5, 0xa8, 0x4f, 0x19, 0x11, 0xb4, 0x59, 0xed, 0xb1,
+	0x40, 0x04, 0xe8, 0xf9, 0x78, 0x54, 0x55, 0x8f, 0xaa, 0xaa, 0x51, 0x55, 0xd2, 0xf3, 0xaa, 0xe1,
+	0xa8, 0xe5, 0x57, 0x2d, 0xec, 0x56, 0xd0, 0x0a, 0xd6, 0xd4, 0xe0, 0xdd, 0xfe, 0x9e, 0xfa, 0x4b,
+	0xfd, 0xa1, 0x7e, 0x69, 0xd0, 0xe5, 0xb7, 0xf7, 0xaf, 0xf3, 0xaa, 0xa7, 0x38, 0xd3, 0x8f, 0x05,
+	0xf5, 0xb9, 0x17, 0xf8, 0xfc, 0x55, 0xd2, 0xf3, 0x38, 0x65, 0x07, 0x94, 0xad, 0xf5, 0xf6, 0x5b,
+	0x92, 0xc6, 0x93, 0x1d, 0xd6, 0x0e, 0x06, 0xa6, 0xb7, 0xfc, 0x7a, 0x8c, 0xd4, 0x25, 0x8d, 0xb6,
+	0xe7, 0x53, 0x76, 0x18, 0x0f, 0xef, 0x52, 0x41, 0xb2, 0x46, 0xad, 0x0d, 0x1b, 0xc5, 0xfa, 0xbe,
+	0xf0, 0xba, 0x74, 0x60, 0xc0, 0x1b, 0x27, 0x0d, 0xe0, 0x8d, 0x36, 0xed, 0x92, 0xf4, 0x38, 0xf7,
+	0x03, 0xb8, 0xb0, 0xee, 0x93, 0xce, 0x21, 0xf7, 0x38, 0xee, 0xfb, 0xeb, 0xac, 0xd5, 0xef, 0x52,
+	0x5f, 0xa0, 0x4b, 0x30, 0xe1, 0x93, 0x2e, 0x5d, 0x72, 0x2e, 0x39, 0x2f, 0x96, 0x6b, 0x33, 0x9f,
+	0x1f, 0xad, 0x9e, 0x3b, 0x3e, 0x5a, 0x9d, 0x78, 0x87, 0x74, 0x29, 0x56, 0x14, 0xf4, 0x0d, 0x98,
+	0x3c, 0x20, 0x9d, 0x3e, 0x5d, 0x2a, 0xa8, 0x2e, 0xb3, 0xa6, 0xcb, 0xe4, 0x03, 0xd9, 0x88, 0x35,
+	0xcd, 0xfd, 0x93, 0x62, 0x02, 0xfe, 0x9b, 0x54, 0x90, 0x26, 0x11, 0x04, 0x75, 0xa1, 0xd4, 0x21,
+	0xbb, 0xb4, 0xc3, 0x97, 0x9c, 0x4b, 0xc5, 0x17, 0x2b, 0x57, 0x6e, 0x55, 0x47, 0xd9, 0xc4, 0x6a,
+	0x06, 0x54, 0x75, 0x5b, 0xe1, 0xdc, 0xf2, 0x05, 0x3b, 0xac, 0x9d, 0x37, 0x93, 0x28, 0xe9, 0x46,
+	0x6c, 0x98, 0xa0, 0x3f, 0x72, 0xa0, 0x42, 0x7c, 0x3f, 0x10, 0x44, 0xc8, 0x6d, 0x5a, 0x2a, 0x28,
+	0xa6, 0x77, 0xc7, 0x67, 0xba, 0x1e, 0x83, 0x69, 0xce, 0x17, 0x0c, 0xe7, 0x8a, 0x45, 0xc1, 0x36,
+	0xcf, 0xe5, 0x37, 0xa1, 0x62, 0x4d, 0x15, 0xcd, 0x43, 0x71, 0x9f, 0x1e, 0x6a, 0xf9, 0x62, 0xf9,
+	0x13, 0x2d, 0x26, 0x04, 0x6a, 0x24, 0x78, 0xa3, 0x70, 0xdd, 0x59, 0xbe, 0x09, 0xf3, 0x69, 0x86,
+	0x79, 0xc6, 0xbb, 0x7f, 0xed, 0xc0, 0xa2, 0xb5, 0x0a, 0x4c, 0xf7, 0x28, 0xa3, 0x7e, 0x83, 0xa2,
+	0x35, 0x28, 0xcb, 0xbd, 0xe4, 0x3d, 0xd2, 0x08, 0xb7, 0x7a, 0xc1, 0x2c, 0xa4, 0xfc, 0x4e, 0x48,
+	0xc0, 0x71, 0x9f, 0x48, 0x2d, 0x0a, 0x8f, 0x52, 0x8b, 0x5e, 0x9b, 0x70, 0xba, 0x54, 0x4c, 0xaa,
+	0xc5, 0x8e, 0x6c, 0xc4, 0x9a, 0xe6, 0xfe, 0x3a, 0x7c, 0x2d, 0x9c, 0xcf, 0x7d, 0xda, 0xed, 0x75,
+	0x88, 0xa0, 0xf1, 0xa4, 0x4e, 0x54, 0x3d, 0x77, 0x0e, 0x66, 0xd7, 0x7b, 0x3d, 0x16, 0x1c, 0xd0,
+	0x66, 0x5d, 0x90, 0x16, 0x75, 0xff, 0xd8, 0x81, 0x8b, 0xeb, 0xac, 0x15, 0x6c, 0x6c, 0xae, 0xf7,
+	0x7a, 0x6f, 0x53, 0xd2, 0x11, 0xed, 0xba, 0x20, 0xa2, 0xcf, 0xd1, 0x4d, 0x28, 0x71, 0xf5, 0xcb,
+	0xc0, 0xbd, 0x10, 0x6a, 0x88, 0xa6, 0x3f, 0x3c, 0x5a, 0x5d, 0xcc, 0x18, 0x48, 0xb1, 0x19, 0x85,
+	0x5e, 0x82, 0xa9, 0x2e, 0xe5, 0x9c, 0xb4, 0xc2, 0x35, 0xcf, 0x19, 0x80, 0xa9, 0x6f, 0xea, 0x66,
+	0x1c, 0xd2, 0xdd, 0x7f, 0x2b, 0xc0, 0x5c, 0x84, 0x65, 0xd8, 0x9f, 0x81, 0x80, 0xfb, 0x30, 0xd3,
+	0xb6, 0x56, 0xa8, 0xe4, 0x5c, 0xb9, 0xf2, 0xd6, 0x88, 0xba, 0x9c, 0x25, 0xa4, 0xda, 0xa2, 0x61,
+	0x33, 0x63, 0xb7, 0xe2, 0x04, 0x1b, 0xd4, 0x05, 0xe0, 0x87, 0x7e, 0xc3, 0x30, 0x9d, 0x50, 0x4c,
+	0xdf, 0xcc, 0xc9, 0xb4, 0x1e, 0x01, 0xd4, 0x90, 0x61, 0x09, 0x71, 0x1b, 0xb6, 0x18, 0xb8, 0xff,
+	0xec, 0xc0, 0x85, 0x8c, 0x71, 0xe8, 0xd7, 0x52, 0xfb, 0xf9, 0xfc, 0xc0, 0x7e, 0xa2, 0x81, 0x61,
+	0xf1, 0x6e, 0xbe, 0x02, 0xd3, 0x8c, 0x1e, 0x78, 0xd2, 0x57, 0x1b, 0x09, 0xcf, 0x9b, 0xf1, 0xd3,
+	0xd8, 0xb4, 0xe3, 0xa8, 0x07, 0x7a, 0x19, 0xca, 0xe1, 0x6f, 0x29, 0xe6, 0xa2, 0x54, 0x67, 0xb9,
+	0x71, 0x61, 0x57, 0x8e, 0x63, 0xba, 0xfb, 0x87, 0x30, 0xb9, 0xd1, 0x26, 0x4c, 0x48, 0x8d, 0x61,
+	0xb4, 0x17, 0xbc, 0x8b, 0xb7, 0xcd, 0x14, 0x23, 0x8d, 0xc1, 0xba, 0x19, 0x87, 0xf4, 0x11, 0x36,
+	0xfb, 0x25, 0x98, 0x3a, 0xa0, 0x4c, 0xcd, 0xb7, 0x98, 0x04, 0x7b, 0xa0, 0x9b, 0x71, 0x48, 0x77,
+	0xff, 0xd3, 0x81, 0x45, 0x35, 0x83, 0x4d, 0x8f, 0x37, 0x82, 0x03, 0xca, 0x0e, 0x31, 0xe5, 0xfd,
+	0xce, 0x29, 0x4f, 0x68, 0x13, 0xe6, 0x39, 0xed, 0x1e, 0x50, 0xb6, 0x11, 0xf8, 0x5c, 0x30, 0xe2,
+	0xf9, 0xc2, 0xcc, 0x6c, 0xc9, 0xf4, 0x9e, 0xaf, 0xa7, 0xe8, 0x78, 0x60, 0x04, 0x7a, 0x11, 0xa6,
+	0xcd, 0xb4, 0xa5, 0x2a, 0x49, 0xc1, 0xce, 0xc8, 0x3d, 0x30, 0x6b, 0xe2, 0x38, 0xa2, 0xba, 0xff,
+	0xe3, 0xc0, 0x82, 0x5a, 0x55, 0xbd, 0xbf, 0xcb, 0x1b, 0xcc, 0xeb, 0x49, 0x17, 0xf8, 0x34, 0x2e,
+	0xe9, 0x26, 0x9c, 0x6f, 0x86, 0x82, 0xdf, 0xf6, 0xba, 0x9e, 0x50, 0x36, 0x32, 0x59, 0x7b, 0xc6,
+	0x60, 0x9c, 0xdf, 0x4c, 0x50, 0x71, 0xaa, 0xb7, 0xfb, 0xb7, 0x45, 0xb8, 0x10, 0x76, 0xa1, 0xcd,
+	0x75, 0x26, 0xbc, 0x3d, 0xd2, 0x10, 0x1c, 0x35, 0x61, 0xa6, 0x19, 0x37, 0x0b, 0x63, 0x79, 0xbf,
+	0x52, 0xd5, 0xc7, 0x7d, 0xd5, 0x3e, 0xee, 0xab, 0xbd, 0xfd, 0x96, 0x6c, 0xe0, 0x55, 0x19, 0x55,
+	0x54, 0x0f, 0x2e, 0x57, 0xef, 0x7b, 0x5d, 0x1a, 0x5b, 0xb7, 0x05, 0x2f, 0x70, 0x02, 0x15, 0xbd,
+	0x07, 0xc5, 0x96, 0x27, 0xcc, 0x61, 0x7c, 0x7d, 0x34, 0xb3, 0xbe, 0xe3, 0xa5, 0x55, 0xad, 0x56,
+	0x31, 0xac, 0x8a, 0x77, 0x3c, 0x81, 0x25, 0x22, 0xda, 0x85, 0x92, 0xd7, 0x25, 0x2d, 0x1a, 0x9e,
+	0xb9, 0x37, 0x46, 0xc3, 0xde, 0x92, 0x63, 0xd2, 0xe8, 0xd1, 0xe9, 0xae, 0xa8, 0x1c, 0x1b, 0x64,
+	0xc9, 0xa3, 0x21, 0x55, 0x44, 0x1b, 0xe9, 0xc8, 0x3c, 0xb2, 0x8c, 0x25, 0xe6, 0xa1, 0xa8, 0x1c,
+	0x1b, 0x64, 0xf7, 0xcb, 0x02, 0xcc, 0xc7, 0xf2, 0xdb, 0x08, 0xba, 0x5d, 0x4f, 0xa0, 0x65, 0x28,
+	0x78, 0x4d, 0xa3, 0x81, 0x60, 0x06, 0x16, 0xb6, 0x36, 0x71, 0xc1, 0x6b, 0xa2, 0x17, 0xa0, 0xb4,
+	0xcb, 0x88, 0xdf, 0x68, 0x1b, 0xcd, 0x8b, 0x80, 0x6b, 0xaa, 0x15, 0x1b, 0x2a, 0x7a, 0x0e, 0x8a,
+	0x82, 0xb4, 0x8c, 0xc2, 0x45, 0xf2, 0xbb, 0x4f, 0x5a, 0x58, 0xb6, 0x4b, 0x4d, 0xe7, 0xfd, 0xdd,
+	0xdf, 0xa1, 0x0d, 0xbd, 0xf3, 0x96, 0xa6, 0xd7, 0x75, 0x33, 0x0e, 0xe9, 0x92, 0x23, 0xe9, 0x8b,
+	0x76, 0xc0, 0x96, 0x26, 0x93, 0x1c, 0xd7, 0x55, 0x2b, 0x36, 0x54, 0x79, 0x26, 0x35, 0xd4, 0xfc,
+	0x05, 0x65, 0x4b, 0xa5, 0xe4, 0x99, 0xb4, 0x11, 0x12, 0x70, 0xdc, 0x07, 0x7d, 0x08, 0x95, 0x06,
+	0xa3, 0x44, 0x04, 0x6c, 0x93, 0x08, 0xba, 0x34, 0x95, 0x5b, 0x03, 0xe7, 0x64, 0x60, 0xb4, 0x11,
+	0x43, 0x60, 0x1b, 0xcf, 0xfd, 0xb9, 0x03, 0x4b, 0xb1, 0x68, 0xd5, 0xde, 0xc6, 0xc1, 0x80, 0x11,
+	0x8f, 0x33, 0x44, 0x3c, 0x2f, 0x40, 0xa9, 0xe9, 0xb5, 0x28, 0x17, 0x69, 0x29, 0x6f, 0xaa, 0x56,
+	0x6c, 0xa8, 0xe8, 0x0a, 0x40, 0xcb, 0x13, 0xc6, 0x39, 0x18, 0x61, 0x47, 0x47, 0xd0, 0x9d, 0x88,
+	0x82, 0xad, 0x5e, 0xe8, 0x3d, 0x28, 0xab, 0x69, 0x8e, 0x69, 0x76, 0xea, 0xa8, 0xd8, 0x08, 0x01,
+	0x70, 0x8c, 0xe5, 0x7e, 0x31, 0x01, 0x53, 0xb7, 0x19, 0xf5, 0x5a, 0x6d, 0x81, 0x7e, 0x1b, 0xa6,
+	0xbb, 0x26, 0xa8, 0x54, 0x8b, 0xac, 0x5c, 0xf9, 0xd5, 0xd1, 0x78, 0xdc, 0x53, 0x9b, 0x2e, 0x03,
+	0xd2, 0x78, 0x21, 0x71, 0x1b, 0x8e, 0x50, 0x65, 0x40, 0x46, 0x3a, 0x1e, 0xe1, 0x6a, 0xdf, 0xac,
+	0x80, 0x6c, 0x5d, 0x36, 0x62, 0x4d, 0x43, 0xdf, 0x81, 0x52, 0xc0, 0xbc, 0x96, 0xe7, 0x2f, 0x95,
+	0xd5, 0x24, 0x5e, 0x1b, 0xcd, 0x84, 0xcc, 0x2a, 0xee, 0xa9, 0xa1, 0xb1, 0xf0, 0xf5, 0xdf, 0xd8,
+	0x40, 0xa2, 0xf7, 0x61, 0x4a, 0x2b, 0x53, 0x68, 0xa0, 0x6b, 0x23, 0x3b, 0x18, 0xad, 0x8f, 0xb1,
+	0xd2, 0xeb, 0xbf, 0x39, 0x0e, 0x01, 0x51, 0x3d, 0xf2, 0x2f, 0x13, 0x0a, 0xfa, 0xe5, 0x1c, 0xfe,
+	0x65, 0xa8, 0x43, 0xa9, 0x47, 0x0e, 0x65, 0x32, 0x0f, 0xa8, 0x72, 0x19, 0xc3, 0x3c, 0x88, 0x14,
+	0xb1, 0x89, 0x5c, 0x4a, 0x63, 0x88, 0xd8, 0x84, 0x4d, 0xe7, 0x93, 0xe1, 0x4e, 0x18, 0xd8, 0xb8,
+	0x9f, 0x14, 0x61, 0xc1, 0xf4, 0xdc, 0x08, 0x3a, 0x1d, 0xda, 0x50, 0xc7, 0xa4, 0xf6, 0x4f, 0xc5,
+	0x4c, 0xff, 0xe4, 0xc1, 0xa4, 0x27, 0x68, 0x37, 0x4c, 0xc0, 0x6a, 0xb9, 0x66, 0x13, 0xf3, 0xa8,
+	0x6e, 0x49, 0x10, 0x9d, 0x03, 0x45, 0xbb, 0x64, 0x7a, 0x61, 0xcd, 0x01, 0xfd, 0x99, 0x03, 0x17,
+	0x0e, 0x28, 0xf3, 0xf6, 0xbc, 0x86, 0xca, 0x60, 0xde, 0xf6, 0xb8, 0x08, 0xd8, 0xa1, 0x39, 0x11,
+	0xde, 0x18, 0x8d, 0xf3, 0x03, 0x0b, 0x60, 0xcb, 0xdf, 0x0b, 0x6a, 0x5f, 0x37, 0xdc, 0x2e, 0x3c,
+	0x18, 0x84, 0xc6, 0x59, 0xfc, 0x96, 0x7b, 0x00, 0xf1, 0x6c, 0x33, 0x12, 0xa8, 0x6d, 0x3b, 0x81,
+	0x1a, 0x79, 0x62, 0xe1, 0x62, 0x43, 0x97, 0x65, 0x27, 0x5e, 0x9f, 0x39, 0x50, 0x31, 0xf4, 0x6d,
+	0x8f, 0x0b, 0xf4, 0xc1, 0x80, 0xb5, 0x57, 0x47, 0xb3, 0x76, 0x39, 0x5a, 0xd9, 0x7a, 0x14, 0xaf,
+	0x86, 0x2d, 0x96, 0xa5, 0xe3, 0x70, 0x4b, 0xb5, 0x60, 0x5f, 0xcd, 0x35, 0xff, 0xd8, 0x31, 0x28,
+	0x19, 0x99, 0xbd, 0x73, 0x19, 0xcc, 0x26, 0x8c, 0x1c, 0x5d, 0x85, 0x89, 0x7d, 0xcf, 0x0f, 0x4f,
+	0xbd, 0x5f, 0x0e, 0xe3, 0xa9, 0xdf, 0xf4, 0xfc, 0xe6, 0xc3, 0xa3, 0xd5, 0x85, 0x44, 0x67, 0xd9,
+	0x88, 0x55, 0xf7, 0x93, 0xc3, 0xb0, 0x1b, 0xd3, 0x3f, 0xfa, 0xfb, 0xd5, 0x73, 0xdf, 0xff, 0xe9,
+	0xa5, 0x73, 0xee, 0xa7, 0x45, 0x98, 0x4f, 0x4b, 0x75, 0x84, 0x82, 0x44, 0xec, 0xc3, 0xa6, 0xcf,
+	0xd4, 0x87, 0x15, 0xce, 0xce, 0x87, 0x15, 0xcf, 0xc2, 0x87, 0x4d, 0x9c, 0x9a, 0x0f, 0x73, 0xff,
+	0xdd, 0x81, 0xf3, 0xd1, 0xce, 0x7c, 0xb7, 0x2f, 0x4f, 0xd6, 0x58, 0xea, 0xce, 0xe9, 0x4b, 0xfd,
+	0x23, 0x98, 0xe2, 0x41, 0x9f, 0x35, 0x54, 0xf8, 0x28, 0xd1, 0x5f, 0xcf, 0xe7, 0x34, 0xf5, 0x58,
+	0x2b, 0x66, 0xd2, 0x0d, 0x38, 0x44, 0x75, 0x3f, 0x88, 0xd6, 0x63, 0x48, 0x3a, 0xa2, 0x60, 0x32,
+	0xde, 0x92, 0xeb, 0x99, 0xb6, 0x23, 0x0a, 0xd9, 0x8a, 0x0d, 0x15, 0xb9, 0xca, 0x9d, 0x87, 0x81,
+	0x6d, 0xb9, 0x06, 0xc6, 0x2b, 0xab, 0x3d, 0xd0, 0x14, 0xf7, 0xe7, 0xc5, 0xc8, 0x7a, 0x4c, 0xfa,
+	0xfa, 0x3d, 0x00, 0xed, 0x99, 0x68, 0x73, 0xcb, 0x37, 0xae, 0x77, 0x63, 0x8c, 0x83, 0xc0, 0xb8,
+	0x43, 0x89, 0xa2, 0x7d, 0x6f, 0x14, 0x03, 0xc4, 0x04, 0x6c, 0xb1, 0x42, 0xbf, 0x07, 0x15, 0x62,
+	0x4a, 0x26, 0xb7, 0x03, 0x66, 0x74, 0x78, 0x73, 0x1c, 0xce, 0xeb, 0x31, 0x4c, 0xba, 0xf4, 0x15,
+	0x53, 0xb0, 0xcd, 0x6d, 0x99, 0xc1, 0x5c, 0x6a, 0xbe, 0x19, 0xde, 0x77, 0x2b, 0xe9, 0x7d, 0x5f,
+	0xcb, 0x73, 0x2c, 0x98, 0x3a, 0x90, 0x5d, 0x33, 0xe3, 0x30, 0x9f, 0x9e, 0xe9, 0xa9, 0x31, 0x4d,
+	0x14, 0x9f, 0x6c, 0x7f, 0xff, 0x77, 0x05, 0x28, 0x47, 0x16, 0x9f, 0x27, 0x4b, 0xd5, 0x27, 0x75,
+	0xe1, 0x84, 0x4c, 0xa2, 0x38, 0x4a, 0x26, 0x31, 0x31, 0x3c, 0x93, 0x08, 0x2b, 0x59, 0xa5, 0x47,
+	0x57, 0xb2, 0xac, 0x4c, 0x62, 0x6a, 0xf4, 0x4c, 0x62, 0xfa, 0xe4, 0x4c, 0xc2, 0xfd, 0x07, 0x07,
+	0xd0, 0x60, 0xda, 0x98, 0x47, 0x50, 0x24, 0xed, 0x87, 0x47, 0x3c, 0xa5, 0xd3, 0xb9, 0xdb, 0x70,
+	0x77, 0xec, 0x7e, 0x36, 0x09, 0x73, 0x77, 0xbc, 0xb1, 0x0b, 0x0e, 0x02, 0x9e, 0xd5, 0x48, 0x75,
+	0x6a, 0x62, 0xa4, 0xba, 0x60, 0x44, 0xd0, 0xd6, 0xa1, 0xd9, 0xdf, 0x1b, 0x66, 0xe8, 0xb3, 0x1b,
+	0xd9, 0xdd, 0x1e, 0x0e, 0x27, 0xe1, 0x61, 0xd0, 0x23, 0x2b, 0xc9, 0x5b, 0x30, 0xcb, 0x05, 0xf3,
+	0x1a, 0x42, 0x97, 0x34, 0xf8, 0x52, 0x45, 0x79, 0xb9, 0x8b, 0xa6, 0xfb, 0x6c, 0xdd, 0x26, 0xe2,
+	0x64, 0xdf, 0xcc, 0x4a, 0xc9, 0x44, 0xee, 0x4a, 0xc9, 0x1a, 0x94, 0x49, 0xa7, 0x13, 0x7c, 0xef,
+	0x3e, 0x69, 0x71, 0x93, 0xaa, 0x46, 0x5a, 0xb3, 0x1e, 0x12, 0x70, 0xdc, 0x07, 0x55, 0x01, 0xbc,
+	0x96, 0x1f, 0x30, 0xaa, 0x46, 0x94, 0x94, 0xbb, 0x3d, 0x2f, 0x7d, 0xdd, 0x56, 0xd4, 0x8a, 0xad,
+	0x1e, 0xa8, 0x0e, 0x17, 0x3d, 0x9f, 0xd3, 0x46, 0x9f, 0xd1, 0xfa, 0xbe, 0xd7, 0xbb, 0xbf, 0x5d,
+	0x57, 0x5e, 0xe2, 0x50, 0x69, 0xf3, 0x74, 0xed, 0x39, 0xc3, 0xec, 0xe2, 0x56, 0x56, 0x27, 0x9c,
+	0x3d, 0x16, 0xbd, 0x0e, 0x33, 0x9e, 0xdf, 0xe8, 0xf4, 0x9b, 0x74, 0x87, 0x88, 0x36, 0x5f, 0x9a,
+	0x56, 0xd3, 0x98, 0x3f, 0x3e, 0x5a, 0x9d, 0xd9, 0xb2, 0xda, 0x71, 0xa2, 0x97, 0x1c, 0x45, 0x3f,
+	0xb6, 0x46, 0x95, 0xe3, 0x51, 0xb7, 0x3e, 0xb6, 0x47, 0xd9, 0xbd, 0x32, 0x6a, 0x49, 0x90, 0xab,
+	0x96, 0xf4, 0xe3, 0x02, 0x94, 0x74, 0x29, 0x17, 0x5d, 0x4d, 0xd5, 0x4b, 0x9f, 0x1b, 0xa8, 0x97,
+	0x56, 0xb2, 0xca, 0xde, 0x2e, 0x94, 0x3c, 0xce, 0xfb, 0xc9, 0xd3, 0x6d, 0x4b, 0xb5, 0x60, 0x43,
+	0x51, 0x65, 0x97, 0xc0, 0xdf, 0xf3, 0x5a, 0x26, 0x39, 0xbe, 0x69, 0x85, 0xb2, 0xf1, 0x95, 0xd8,
+	0x47, 0xd1, 0x9d, 0x59, 0x1c, 0xd5, 0x26, 0x3a, 0xc8, 0xf0, 0xf6, 0x6e, 0xfd, 0xde, 0x3b, 0x9a,
+	0xc7, 0x86, 0x42, 0xc4, 0x06, 0x59, 0xf2, 0x08, 0xfa, 0xa2, 0xd7, 0x17, 0x4a, 0x51, 0x4e, 0x89,
+	0xc7, 0x3d, 0x85, 0x88, 0x0d, 0xb2, 0xfb, 0xa9, 0x03, 0x73, 0x5a, 0x06, 0x1b, 0x6d, 0xda, 0xd8,
+	0xaf, 0x0b, 0xda, 0x93, 0xd1, 0x66, 0x9f, 0x53, 0x9e, 0x8e, 0x36, 0xdf, 0xe5, 0x94, 0x63, 0x45,
+	0xb1, 0x56, 0x5f, 0x38, 0xab, 0xd5, 0xbb, 0xff, 0xe4, 0xc0, 0xa4, 0x0a, 0xeb, 0xf2, 0xf8, 0x9f,
+	0x64, 0xa9, 0xa3, 0x30, 0x52, 0xa9, 0xe3, 0x84, 0x22, 0x54, 0x5c, 0x65, 0x99, 0x78, 0x54, 0x95,
+	0xc5, 0xfd, 0x99, 0x03, 0x8b, 0x59, 0x95, 0xbb, 0x3c, 0xd3, 0x7f, 0x05, 0xa6, 0x7b, 0x1d, 0x22,
+	0xf6, 0x02, 0xd6, 0x4d, 0x97, 0xe8, 0x77, 0x4c, 0x3b, 0x8e, 0x7a, 0x20, 0x06, 0xc0, 0xc2, 0x14,
+	0x21, 0x0c, 0x9f, 0x6f, 0xe6, 0x3d, 0x11, 0x92, 0x25, 0xa7, 0x58, 0x58, 0x51, 0x13, 0xc7, 0x16,
+	0x17, 0xf7, 0x2f, 0x26, 0x61, 0x41, 0x0d, 0x19, 0xf7, 0x84, 0x18, 0x67, 0x87, 0x7a, 0xf0, 0x8c,
+	0x0a, 0xec, 0x07, 0x0f, 0x15, 0xbd, 0x69, 0xd7, 0xcd, 0xf8, 0x67, 0xb6, 0x32, 0x7b, 0x3d, 0x1c,
+	0x4a, 0xc1, 0x43, 0x70, 0x07, 0x4f, 0x0a, 0xf8, 0xc5, 0x3b, 0x29, 0x6c, 0x65, 0x9b, 0x3a, 0x51,
+	0xd9, 0x86, 0x9e, 0x2b, 0xd3, 0x8f, 0x71, 0xae, 0x0c, 0xfa, 0xfa, 0x72, 0x2e, 0x5f, 0xff, 0x37,
+	0x05, 0x98, 0xda, 0x61, 0x81, 0xaa, 0x00, 0x9f, 0x7d, 0x31, 0xf1, 0x1e, 0x4c, 0xf0, 0x1e, 0x6d,
+	0x18, 0x9f, 0x77, 0x79, 0x34, 0x4b, 0x33, 0xd3, 0xab, 0xf7, 0x68, 0xa3, 0x36, 0x2d, 0xdd, 0xa8,
+	0xfc, 0x85, 0x15, 0x90, 0x55, 0x15, 0x2b, 0xe6, 0x89, 0xc0, 0x43, 0xc8, 0x47, 0x57, 0xc5, 0x3e,
+	0x73, 0xa0, 0x62, 0x7a, 0x3e, 0xb5, 0xe5, 0x17, 0x33, 0xbf, 0x21, 0xe5, 0x97, 0xbf, 0x8a, 0x57,
+	0x20, 0x85, 0x86, 0xfe, 0x00, 0x16, 0x7a, 0x2c, 0xe8, 0x06, 0xd2, 0x52, 0x77, 0x82, 0x8e, 0xd7,
+	0xf0, 0x68, 0x58, 0xc1, 0xbb, 0x3a, 0x32, 0x3f, 0x6b, 0xf8, 0x61, 0xed, 0x6b, 0x86, 0xef, 0xc2,
+	0x4e, 0x1a, 0x17, 0x0f, 0xb2, 0x72, 0xff, 0xcb, 0x81, 0xd9, 0x84, 0xec, 0x51, 0x03, 0xa0, 0x11,
+	0xf8, 0x4d, 0x4f, 0x44, 0xb7, 0xa4, 0x95, 0x2b, 0x6b, 0xa3, 0x49, 0x75, 0x23, 0x1c, 0x17, 0x2b,
+	0x5d, 0xd4, 0xc4, 0xb1, 0x05, 0x8b, 0x5e, 0x0b, 0x1f, 0x15, 0x24, 0x83, 0x18, 0xfd, 0xa8, 0xe0,
+	0xe1, 0xd1, 0xea, 0x8c, 0x99, 0x93, 0xfd, 0xc8, 0x20, 0xcf, 0xd5, 0xfd, 0x3f, 0x16, 0xa0, 0x1c,
+	0xad, 0xff, 0x09, 0x98, 0xd1, 0xbb, 0x09, 0x33, 0x7a, 0x2d, 0xe7, 0xce, 0x29, 0x43, 0x8a, 0x62,
+	0x12, 0xcb, 0x98, 0x3e, 0x4c, 0x19, 0x53, 0x5e, 0x95, 0x38, 0xc1, 0x9c, 0xfe, 0x55, 0x6f, 0xbe,
+	0xee, 0xfb, 0x04, 0x0c, 0xea, 0x7e, 0xd2, 0xa0, 0xd6, 0x72, 0xae, 0x66, 0x88, 0x49, 0xfd, 0xc0,
+	0x81, 0xb9, 0x94, 0x11, 0xa0, 0x6f, 0xc0, 0xa4, 0x2a, 0xd9, 0x18, 0xfd, 0x8a, 0x06, 0x9a, 0x04,
+	0x5f, 0xd1, 0xd0, 0x0e, 0x2c, 0x92, 0xbe, 0x08, 0xa2, 0xb1, 0xb7, 0x7c, 0xb2, 0xdb, 0xa1, 0x3a,
+	0x6b, 0x9f, 0xae, 0xfd, 0x92, 0x19, 0xb3, 0xb8, 0x9e, 0xd1, 0x07, 0x67, 0x8e, 0x74, 0x7f, 0x52,
+	0x00, 0x14, 0x35, 0xe6, 0x29, 0x75, 0x7e, 0x08, 0x53, 0x7b, 0xba, 0x28, 0xf3, 0x78, 0xb5, 0xea,
+	0x5a, 0xc5, 0x2e, 0xd7, 0x87, 0x98, 0xe8, 0xdb, 0xa7, 0xa3, 0x47, 0x30, 0xa8, 0x43, 0xe8, 0x7d,
+	0x80, 0x3d, 0xcf, 0xf7, 0x78, 0x7b, 0xcc, 0x5b, 0x35, 0x75, 0x9a, 0xdf, 0x8e, 0x10, 0xb0, 0x85,
+	0xe6, 0x7e, 0x52, 0xb0, 0xf4, 0x53, 0xb9, 0xcb, 0x91, 0xf6, 0xf5, 0xa5, 0xa4, 0x30, 0xcb, 0x83,
+	0xf7, 0x18, 0x96, 0x60, 0x26, 0x0e, 0x08, 0x0b, 0x4b, 0xaa, 0xd7, 0x72, 0x8a, 0xe5, 0x01, 0x61,
+	0x9e, 0xdc, 0xf8, 0x78, 0x4b, 0x1f, 0x10, 0xc6, 0xb1, 0x82, 0x44, 0xdf, 0x92, 0x53, 0xa5, 0xbd,
+	0xd0, 0x85, 0xe6, 0xf6, 0x09, 0x82, 0xf6, 0xec, 0xf5, 0xd1, 0x1e, 0xc7, 0x1a, 0xd0, 0xfd, 0x64,
+	0xca, 0x52, 0x78, 0xe3, 0xb5, 0xef, 0x02, 0xea, 0x10, 0x2e, 0xde, 0x26, 0x7e, 0x53, 0xaa, 0x27,
+	0xdd, 0x63, 0x94, 0xb7, 0x4d, 0x84, 0xb6, 0x6c, 0x50, 0xd0, 0xf6, 0x40, 0x0f, 0x9c, 0x31, 0x0a,
+	0x5d, 0x4d, 0x3a, 0xe7, 0xd5, 0xb4, 0x73, 0x3e, 0x1f, 0x5b, 0xdb, 0x78, 0xee, 0xd9, 0x56, 0xf7,
+	0xc9, 0x33, 0x50, 0xf7, 0xdf, 0x87, 0x85, 0xbd, 0xf4, 0xbd, 0x96, 0xb9, 0xe5, 0xbe, 0x36, 0xe6,
+	0xb5, 0x58, 0xed, 0xe2, 0x71, 0x7c, 0x19, 0x12, 0x37, 0xe3, 0x41, 0x46, 0x28, 0x08, 0xdf, 0x73,
+	0xa9, 0xec, 0x53, 0x17, 0x16, 0x46, 0x36, 0xb9, 0x54, 0xde, 0x9a, 0x7e, 0xc9, 0xa5, 0x21, 0x71,
+	0x82, 0x41, 0xca, 0x04, 0x4b, 0xa7, 0x69, 0x82, 0xe8, 0x2a, 0x54, 0x1a, 0x7d, 0xc6, 0xa8, 0x2f,
+	0xe4, 0x74, 0x54, 0x28, 0x5b, 0x8c, 0x0b, 0xc4, 0x1b, 0x31, 0x09, 0xdb, 0xfd, 0xd0, 0x0f, 0x1d,
+	0xb8, 0x28, 0x95, 0xf5, 0xd6, 0xc7, 0xb4, 0xd1, 0x97, 0x52, 0x09, 0x1f, 0x5a, 0x2e, 0x55, 0x94,
+	0x34, 0x46, 0x7c, 0xdd, 0x56, 0xcf, 0x82, 0x88, 0xe3, 0xf2, 0x4c, 0x32, 0xce, 0x66, 0x8c, 0x3e,
+	0x52, 0xae, 0x43, 0x50, 0x95, 0xf6, 0x3c, 0x7e, 0x7a, 0x5f, 0x36, 0x6e, 0x47, 0x68, 0xb7, 0x23,
+	0xa8, 0xfb, 0x97, 0x09, 0x6f, 0x35, 0x5a, 0xd1, 0x61, 0x19, 0x0a, 0x84, 0xa7, 0xcb, 0xc4, 0xeb,
+	0x1c, 0x17, 0x08, 0x47, 0xdf, 0x86, 0x49, 0x46, 0x05, 0x3b, 0x34, 0x4e, 0xf5, 0xfa, 0x18, 0x0e,
+	0x04, 0xcb, 0xf1, 0x7a, 0xaa, 0xea, 0x27, 0xd6, 0x88, 0x56, 0xad, 0xa3, 0x78, 0x66, 0xb5, 0x8e,
+	0x1f, 0x3b, 0xd6, 0x59, 0x18, 0x4d, 0x06, 0xbd, 0x0b, 0x53, 0xc2, 0xeb, 0xd2, 0xa0, 0x2f, 0xf2,
+	0x05, 0x18, 0x9b, 0x7d, 0xa6, 0xee, 0x7e, 0xb5, 0xc9, 0xdf, 0xd7, 0x10, 0x38, 0xc4, 0x92, 0x59,
+	0x17, 0x65, 0x2c, 0x60, 0xf7, 0xdb, 0xd2, 0x85, 0x05, 0x1d, 0x7d, 0x8a, 0xcf, 0xc6, 0x59, 0xd7,
+	0xad, 0x04, 0x15, 0xa7, 0x7a, 0xbb, 0x0c, 0xe2, 0x78, 0x39, 0x7c, 0xc1, 0x8a, 0x3e, 0x34, 0x51,
+	0x9d, 0x93, 0xe7, 0x45, 0xe6, 0x00, 0xcc, 0xb0, 0xe8, 0xce, 0xfd, 0x89, 0x03, 0x17, 0x33, 0x7b,
+	0x47, 0xc7, 0x52, 0xe1, 0x0c, 0x8f, 0x25, 0xe7, 0xb4, 0x8f, 0xa5, 0xf7, 0x2d, 0x11, 0x86, 0x53,
+	0x38, 0xad, 0x67, 0xe7, 0x3f, 0x2a, 0xc0, 0x3c, 0xa6, 0xbd, 0x20, 0x51, 0xa1, 0xd9, 0x09, 0xdf,
+	0xb8, 0xe5, 0x08, 0x69, 0x52, 0xf7, 0x00, 0xb5, 0xa9, 0xc4, 0xe3, 0xb6, 0x6f, 0xc1, 0xa4, 0x2a,
+	0x9e, 0x98, 0x20, 0xec, 0x5a, 0x8e, 0x7b, 0xdb, 0x04, 0xaa, 0xb2, 0x38, 0x5d, 0x85, 0xd2, 0x80,
+	0x12, 0x59, 0x5d, 0xb9, 0x1a, 0x83, 0xbb, 0x96, 0xe3, 0xf2, 0x76, 0x10, 0x59, 0x35, 0x63, 0x0d,
+	0xe8, 0x7e, 0x5a, 0x00, 0x1d, 0xfe, 0x3c, 0x81, 0x34, 0xe7, 0xb7, 0x12, 0x69, 0xce, 0xda, 0xa8,
+	0x4e, 0x5c, 0x8a, 0x67, 0x58, 0x8a, 0x93, 0x0e, 0x4d, 0x2f, 0xe7, 0x01, 0x7d, 0x74, 0x7a, 0xf3,
+	0x2f, 0x0e, 0x94, 0x55, 0xbf, 0x27, 0x90, 0xda, 0xec, 0x24, 0x53, 0x9b, 0x97, 0x73, 0xac, 0x62,
+	0x48, 0x5a, 0xf3, 0x49, 0xd1, 0xcc, 0x3e, 0x0a, 0x7c, 0xdb, 0x84, 0x35, 0x4d, 0x48, 0x17, 0x5b,
+	0xa0, 0x6c, 0xc4, 0x9a, 0x86, 0x7e, 0x17, 0xe6, 0x99, 0xbe, 0xc5, 0xa7, 0xcd, 0xdb, 0x51, 0x7c,
+	0x55, 0xcc, 0x7d, 0xcd, 0x6e, 0x9e, 0x02, 0xc4, 0xa5, 0x3d, 0x9c, 0x42, 0xc5, 0x03, 0x7c, 0x64,
+	0xcc, 0xd5, 0x4b, 0xfb, 0x32, 0x13, 0x8b, 0x5c, 0x1b, 0xd3, 0x71, 0xea, 0x98, 0x6b, 0xa0, 0x19,
+	0x0f, 0x32, 0x42, 0x6d, 0x98, 0xb1, 0x1f, 0x08, 0x19, 0x5d, 0xba, 0x92, 0xff, 0x25, 0x92, 0xbe,
+	0xca, 0xb1, 0x5b, 0x70, 0x02, 0xd9, 0x3d, 0x2a, 0x41, 0xc5, 0x52, 0xbe, 0x54, 0xb9, 0x64, 0xf6,
+	0x6c, 0xca, 0x25, 0xd9, 0xd1, 0x7d, 0x65, 0xac, 0xe8, 0xfe, 0x72, 0x32, 0xba, 0xff, 0x7a, 0x3a,
+	0xba, 0x07, 0xb5, 0xba, 0x44, 0x64, 0xcf, 0xe1, 0xbc, 0x09, 0x73, 0xc3, 0x97, 0x5e, 0xb9, 0xf2,
+	0xa5, 0xc1, 0x60, 0x1a, 0xc9, 0x13, 0xf9, 0x76, 0x02, 0x12, 0xa7, 0x58, 0xc8, 0x13, 0xdd, 0xb4,
+	0xd4, 0xfb, 0xdd, 0x2e, 0x61, 0x87, 0x4b, 0x33, 0x6a, 0xc2, 0xd1, 0x89, 0x7e, 0x3b, 0x41, 0xc5,
+	0xa9, 0xde, 0x68, 0x07, 0x4a, 0x3a, 0x4a, 0x36, 0xaf, 0x87, 0x5e, 0xc9, 0x13, 0x80, 0xeb, 0x88,
+	0x46, 0xff, 0xc6, 0x06, 0xc7, 0x4e, 0x70, 0xca, 0x27, 0x24, 0x38, 0x77, 0x01, 0x05, 0xbb, 0x2a,
+	0x76, 0x6a, 0xde, 0xd1, 0xdf, 0x67, 0x49, 0xad, 0x2c, 0xa9, 0xe8, 0x39, 0xda, 0xb0, 0x7b, 0x03,
+	0x3d, 0x70, 0xc6, 0x28, 0x69, 0xd5, 0x26, 0xb4, 0x8e, 0x4c, 0xc1, 0x24, 0x33, 0x79, 0x43, 0xc2,
+	0x38, 0x6f, 0x5a, 0x94, 0x56, 0xbd, 0x91, 0x42, 0xc5, 0x03, 0x7c, 0xd0, 0x77, 0x61, 0x56, 0xaa,
+	0x50, 0xcc, 0x18, 0x1e, 0x93, 0xf1, 0xc2, 0xf1, 0xd1, 0xea, 0xec, 0xb6, 0x0d, 0x89, 0x93, 0x1c,
+	0xdc, 0x3f, 0x2f, 0x42, 0x76, 0x60, 0x1f, 0x3f, 0x7c, 0x75, 0x1e, 0xf1, 0xf0, 0xf5, 0x3d, 0x28,
+	0x73, 0x41, 0x98, 0x7e, 0xe4, 0x5b, 0x18, 0xef, 0x91, 0x6f, 0x3d, 0x04, 0xc0, 0x31, 0x56, 0x2a,
+	0xcb, 0x2a, 0x9e, 0x6a, 0x96, 0x75, 0x05, 0x40, 0xc5, 0xa3, 0x1b, 0x41, 0xdf, 0xdc, 0xab, 0xcc,
+	0xc6, 0x3e, 0xe1, 0x56, 0x44, 0xc1, 0x56, 0x2f, 0x74, 0x3d, 0x3a, 0x38, 0xf5, 0x45, 0xca, 0xa5,
+	0x81, 0x8b, 0xe0, 0x74, 0x9e, 0x9e, 0xf1, 0x09, 0xd4, 0x09, 0x0f, 0x47, 0xdc, 0xff, 0x2b, 0x40,
+	0xc2, 0x19, 0xa2, 0x1f, 0x38, 0xb0, 0x40, 0x52, 0x5f, 0x7a, 0x85, 0xb1, 0xe4, 0x6f, 0xe4, 0xfb,
+	0xfc, 0x6e, 0xe0, 0x43, 0xb1, 0xb8, 0x74, 0x9d, 0xee, 0xc2, 0xf1, 0x20, 0x53, 0xf4, 0xa7, 0x0e,
+	0x5c, 0x20, 0x83, 0x9f, 0xf2, 0x99, 0x4d, 0x7f, 0x73, 0xec, 0x6f, 0x01, 0x6b, 0xcf, 0x1e, 0x1f,
+	0xad, 0x66, 0x7d, 0xe4, 0x88, 0xb3, 0xd8, 0xa1, 0xef, 0xc0, 0x04, 0x61, 0xad, 0xb0, 0xcc, 0x93,
+	0x9f, 0x6d, 0xf8, 0x85, 0x66, 0x1c, 0x1d, 0xad, 0xb3, 0x16, 0xc7, 0x0a, 0xd4, 0xfd, 0x69, 0x11,
+	0xe6, 0xd3, 0x0f, 0x65, 0xcd, 0xdb, 0xa2, 0x89, 0xcc, 0xb7, 0x45, 0xd2, 0x46, 0x1a, 0x22, 0x7a,
+	0xe8, 0x13, 0xdb, 0x88, 0x6c, 0xc4, 0x9a, 0x16, 0xd9, 0x88, 0xd4, 0x4b, 0x53, 0x80, 0x19, 0xcf,
+	0x46, 0xe4, 0x9f, 0x38, 0xc6, 0x42, 0xd7, 0x93, 0x67, 0x8b, 0x9b, 0x3e, 0x5b, 0x16, 0xec, 0xb5,
+	0x8c, 0x5b, 0x3c, 0xea, 0x42, 0xc5, 0xda, 0x07, 0x63, 0x89, 0x37, 0x72, 0xcb, 0x3d, 0x56, 0xbb,
+	0x39, 0xfd, 0x99, 0x67, 0x4c, 0xb1, 0xf1, 0x63, 0xbb, 0x57, 0xd2, 0x7a, 0xac, 0xea, 0x8a, 0x12,
+	0x97, 0x85, 0xe6, 0xce, 0xc1, 0x6c, 0xe2, 0xbd, 0x9b, 0xba, 0xb7, 0x78, 0x8f, 0x30, 0xda, 0x0e,
+	0xfa, 0x9c, 0x3e, 0xad, 0xf7, 0x16, 0xd1, 0x04, 0x4f, 0xfb, 0xde, 0x22, 0x06, 0x3e, 0xf9, 0xde,
+	0x22, 0xea, 0xfb, 0xd4, 0xde, 0x5b, 0x44, 0x33, 0x1c, 0x12, 0xe0, 0xff, 0x6f, 0xc1, 0x5a, 0x45,
+	0x32, 0xc8, 0x2f, 0x3c, 0x22, 0xc8, 0xff, 0x00, 0xa6, 0x3d, 0x5f, 0x50, 0x76, 0x40, 0x3a, 0xa6,
+	0x32, 0x94, 0xb7, 0x82, 0x12, 0x2d, 0x75, 0xcb, 0xe0, 0xe0, 0x08, 0x11, 0x75, 0xe0, 0x62, 0x58,
+	0xd1, 0x64, 0x94, 0xc4, 0x37, 0x2a, 0xe6, 0x55, 0xc2, 0x1b, 0x61, 0xe9, 0xed, 0x76, 0x56, 0xa7,
+	0x87, 0xc3, 0x08, 0x38, 0x1b, 0x14, 0x71, 0x98, 0xe5, 0x56, 0x76, 0x1b, 0x1e, 0x24, 0x23, 0x56,
+	0x83, 0xd3, 0x05, 0x01, 0xeb, 0x29, 0x83, 0x0d, 0x8a, 0x93, 0x3c, 0xdc, 0xff, 0x28, 0xc2, 0x5c,
+	0x4a, 0xd3, 0x52, 0x51, 0x7c, 0xf9, 0x49, 0x46, 0xf1, 0xa5, 0xb1, 0xa2, 0xf8, 0xec, 0x00, 0x73,
+	0x62, 0xac, 0x00, 0xf3, 0x2d, 0x1d, 0xe4, 0x99, 0x9d, 0xdb, 0xda, 0x34, 0x2f, 0x3f, 0x23, 0x69,
+	0x6e, 0xdb, 0x44, 0x9c, 0xec, 0xab, 0x4e, 0xe1, 0xe6, 0xe0, 0x67, 0x8e, 0x26, 0x42, 0x7d, 0x33,
+	0xef, 0xd3, 0x9d, 0x08, 0x40, 0x9f, 0xc2, 0x19, 0x04, 0x9c, 0xc5, 0xae, 0x76, 0xf7, 0xf3, 0xaf,
+	0x56, 0xce, 0x7d, 0xf1, 0xd5, 0xca, 0xb9, 0x2f, 0xbf, 0x5a, 0x39, 0xf7, 0xfd, 0xe3, 0x15, 0xe7,
+	0xf3, 0xe3, 0x15, 0xe7, 0x8b, 0xe3, 0x15, 0xe7, 0xcb, 0xe3, 0x15, 0xe7, 0xbf, 0x8f, 0x57, 0x9c,
+	0x1f, 0xfe, 0x6c, 0xe5, 0xdc, 0xfb, 0xcf, 0x8f, 0xf2, 0xff, 0x28, 0xfe, 0x3f, 0x00, 0x00, 0xff,
+	0xff, 0xd5, 0xc2, 0x59, 0x51, 0xb6, 0x42, 0x00, 0x00,
 }
 
 func (m *AnalysisRunArgument) Marshal() (dAtA []byte, err error) {
@@ -3993,9 +4029,20 @@ func (m *PromotionStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
-	i = encodeVarintGenerated(dAtA, i, uint64(m.CurrentStepAttempt))
-	i--
-	dAtA[i] = 0x58
+	if len(m.StepExecutionMetadata) > 0 {
+		for iNdEx := len(m.StepExecutionMetadata) - 1; iNdEx >= 0; iNdEx-- {
+			{
+				size, err := m.StepExecutionMetadata[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				if err != nil {
+					return 0, err
+				}
+				i -= size
+				i = encodeVarintGenerated(dAtA, i, uint64(size))
+			}
+			i--
+			dAtA[i] = 0x5a
+		}
+	}
 	if m.State != nil {
 		{
 			size, err := m.State.MarshalToSizedBuffer(dAtA[:i])
@@ -4156,9 +4203,21 @@ func (m *PromotionStepRetry) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
-	i = encodeVarintGenerated(dAtA, i, uint64(m.Attempts))
+	i = encodeVarintGenerated(dAtA, i, uint64(m.ErrorThreshold))
 	i--
-	dAtA[i] = 0x8
+	dAtA[i] = 0x10
+	if m.Timeout != nil {
+		{
+			size, err := m.Timeout.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintGenerated(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0xa
+	}
 	return len(dAtA) - i, nil
 }
 
@@ -4614,6 +4673,71 @@ func (m *StageStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *StepExecutionMetadata) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *StepExecutionMetadata) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *StepExecutionMetadata) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	i -= len(m.Message)
+	copy(dAtA[i:], m.Message)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Message)))
+	i--
+	dAtA[i] = 0x32
+	i -= len(m.Status)
+	copy(dAtA[i:], m.Status)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Status)))
+	i--
+	dAtA[i] = 0x2a
+	i = encodeVarintGenerated(dAtA, i, uint64(m.ErrorCount))
+	i--
+	dAtA[i] = 0x20
+	if m.FinishedAt != nil {
+		{
+			size, err := m.FinishedAt.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintGenerated(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x1a
+	}
+	if m.StartedAt != nil {
+		{
+			size, err := m.StartedAt.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintGenerated(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x12
+	}
+	i -= len(m.Alias)
+	copy(dAtA[i:], m.Alias)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Alias)))
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
 func (m *Verification) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
@@ -5815,7 +5939,12 @@ func (m *PromotionStatus) Size() (n int) {
 		l = m.State.Size()
 		n += 1 + l + sovGenerated(uint64(l))
 	}
-	n += 1 + sovGenerated(uint64(m.CurrentStepAttempt))
+	if len(m.StepExecutionMetadata) > 0 {
+		for _, e := range m.StepExecutionMetadata {
+			l = e.Size()
+			n += 1 + l + sovGenerated(uint64(l))
+		}
+	}
 	return n
 }
 
@@ -5846,7 +5975,11 @@ func (m *PromotionStepRetry) Size() (n int) {
 	}
 	var l int
 	_ = l
-	n += 1 + sovGenerated(uint64(m.Attempts))
+	if m.Timeout != nil {
+		l = m.Timeout.Size()
+		n += 1 + l + sovGenerated(uint64(l))
+	}
+	n += 1 + sovGenerated(uint64(m.ErrorThreshold))
 	return n
 }
 
@@ -6015,6 +6148,30 @@ func (m *StageStatus) Size() (n int) {
 	return n
 }
 
+func (m *StepExecutionMetadata) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.Alias)
+	n += 1 + l + sovGenerated(uint64(l))
+	if m.StartedAt != nil {
+		l = m.StartedAt.Size()
+		n += 1 + l + sovGenerated(uint64(l))
+	}
+	if m.FinishedAt != nil {
+		l = m.FinishedAt.Size()
+		n += 1 + l + sovGenerated(uint64(l))
+	}
+	n += 1 + sovGenerated(uint64(m.ErrorCount))
+	l = len(m.Status)
+	n += 1 + l + sovGenerated(uint64(l))
+	l = len(m.Message)
+	n += 1 + l + sovGenerated(uint64(l))
+	return n
+}
+
 func (m *Verification) Size() (n int) {
 	if m == nil {
 		return 0
@@ -6790,6 +6947,11 @@ func (this *PromotionStatus) String() string {
 		repeatedStringForHealthChecks += strings.Replace(strings.Replace(f.String(), "HealthCheckStep", "HealthCheckStep", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForHealthChecks += "}"
+	repeatedStringForStepExecutionMetadata := "[]StepExecutionMetadata{"
+	for _, f := range this.StepExecutionMetadata {
+		repeatedStringForStepExecutionMetadata += strings.Replace(strings.Replace(f.String(), "StepExecutionMetadata", "StepExecutionMetadata", 1), `&`, ``, 1) + ","
+	}
+	repeatedStringForStepExecutionMetadata += "}"
 	s := strings.Join([]string{`&PromotionStatus{`,
 		`Phase:` + fmt.Sprintf("%v", this.Phase) + `,`,
 		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
@@ -6800,7 +6962,7 @@ func (this *PromotionStatus) String() string {
 		`HealthChecks:` + repeatedStringForHealthChecks + `,`,
 		`CurrentStep:` + fmt.Sprintf("%v", this.CurrentStep) + `,`,
 		`State:` + strings.Replace(fmt.Sprintf("%v", this.State), "JSON", "v11.JSON", 1) + `,`,
-		`CurrentStepAttempt:` + fmt.Sprintf("%v", this.CurrentStepAttempt) + `,`,
+		`StepExecutionMetadata:` + repeatedStringForStepExecutionMetadata + `,`,
 		`}`,
 	}, "")
 	return s
@@ -6823,7 +6985,8 @@ func (this *PromotionStepRetry) String() string {
 		return "nil"
 	}
 	s := strings.Join([]string{`&PromotionStepRetry{`,
-		`Attempts:` + fmt.Sprintf("%v", this.Attempts) + `,`,
+		`Timeout:` + strings.Replace(fmt.Sprintf("%v", this.Timeout), "Duration", "v1.Duration", 1) + `,`,
+		`ErrorThreshold:` + fmt.Sprintf("%v", this.ErrorThreshold) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -6957,6 +7120,21 @@ func (this *StageStatus) String() string {
 	}, "")
 	return s
 }
+func (this *StepExecutionMetadata) String() string {
+	if this == nil {
+		return "nil"
+	}
+	s := strings.Join([]string{`&StepExecutionMetadata{`,
+		`Alias:` + fmt.Sprintf("%v", this.Alias) + `,`,
+		`StartedAt:` + strings.Replace(fmt.Sprintf("%v", this.StartedAt), "Time", "v1.Time", 1) + `,`,
+		`FinishedAt:` + strings.Replace(fmt.Sprintf("%v", this.FinishedAt), "Time", "v1.Time", 1) + `,`,
+		`ErrorCount:` + fmt.Sprintf("%v", this.ErrorCount) + `,`,
+		`Status:` + fmt.Sprintf("%v", this.Status) + `,`,
+		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
+		`}`,
+	}, "")
+	return s
+}
 func (this *Verification) String() string {
 	if this == nil {
 		return "nil"
@@ -14131,10 +14309,10 @@ func (m *PromotionStatus) Unmarshal(dAtA []byte) error {
 			}
 			iNdEx = postIndex
 		case 11:
-			if wireType != 0 {
-				return fmt.Errorf("proto: wrong wireType = %d for field CurrentStepAttempt", wireType)
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field StepExecutionMetadata", wireType)
 			}
-			m.CurrentStepAttempt = 0
+			var msglen int
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowGenerated
@@ -14144,11 +14322,26 @@ func (m *PromotionStatus) Unmarshal(dAtA []byte) error {
 				}
 				b := dAtA[iNdEx]
 				iNdEx++
-				m.CurrentStepAttempt |= int64(b&0x7F) << shift
+				msglen |= int(b&0x7F) << shift
 				if b < 0x80 {
 					break
 				}
 			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.StepExecutionMetadata = append(m.StepExecutionMetadata, StepExecutionMetadata{})
+			if err := m.StepExecutionMetadata[len(m.StepExecutionMetadata)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
@@ -14386,10 +14579,46 @@ func (m *PromotionStepRetry) Unmarshal(dAtA []byte) error {
 		}
 		switch fieldNum {
 		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Timeout", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Timeout == nil {
+				m.Timeout = &v1.Duration{}
+			}
+			if err := m.Timeout.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
 			if wireType != 0 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Attempts", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field ErrorThreshold", wireType)
 			}
-			m.Attempts = 0
+			m.ErrorThreshold = 0
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowGenerated
@@ -14399,7 +14628,7 @@ func (m *PromotionStepRetry) Unmarshal(dAtA []byte) error {
 				}
 				b := dAtA[iNdEx]
 				iNdEx++
-				m.Attempts |= int64(b&0x7F) << shift
+				m.ErrorThreshold |= uint32(b&0x7F) << shift
 				if b < 0x80 {
 					break
 				}
@@ -15725,6 +15954,243 @@ func (m *StageStatus) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *StepExecutionMetadata) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowGenerated
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: StepExecutionMetadata: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: StepExecutionMetadata: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Alias", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Alias = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field StartedAt", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.StartedAt == nil {
+				m.StartedAt = &v1.Time{}
+			}
+			if err := m.StartedAt.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field FinishedAt", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.FinishedAt == nil {
+				m.FinishedAt = &v1.Time{}
+			}
+			if err := m.FinishedAt.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 4:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ErrorCount", wireType)
+			}
+			m.ErrorCount = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.ErrorCount |= uint32(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 5:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Status", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Status = PromotionPhase(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 6:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Message", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Message = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipGenerated(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func (m *Verification) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto
index ec301e11..13d69dd2 100644
--- a/api/v1alpha1/generated.proto
+++ b/api/v1alpha1/generated.proto
@@ -897,9 +897,9 @@ message PromotionStatus {
   // subsequent reconciliations attempts.
   optional int64 currentStep = 9;
 
-  // CurrentStepAttempt is the number of times the current step has been
-  // attempted.
-  optional int64 currentStepAttempt = 11;
+  // StepExecutionMetadata tracks metadata pertaining to the execution
+  // of individual promotion steps.
+  repeated StepExecutionMetadata stepExecutionMetadata = 11;
 
   // State stores the state of the promotion process between reconciliation
   // attempts.
@@ -928,15 +928,42 @@ message PromotionStep {
 
 // PromotionStepRetry describes the retry policy for a PromotionStep.
 message PromotionStepRetry {
-  // Attempts is the number of times the step can be attempted before the
-  // PromotionStep is marked as failed.
+  // Timeout is the soft maximum interval in which a step that returns a Running
+  // status (which typically indicates it's waiting for something to happen)
+  // may be retried.
   //
-  // If this field is set to 1, the step will not be retried. If this
-  // field is set to -1, the step will be retried indefinitely.
+  // The maximum is a soft one because the check for whether the interval has
+  // elapsed occurs AFTER the step has run. This effectively means a step may
+  // run ONCE beyond the close of the interval.
   //
-  // The default of this field depends on the step being executed. Refer to
-  // the documentation for the specific step for more information.
-  optional int64 attempts = 1;
+  // If this field is set to nil, the effective default will be a step-specific
+  // one. If no step-specific default exists (i.e. is also nil), the effective
+  // default will be the system-wide default of 0.
+  //
+  // A value of 0 will cause the step to be retried indefinitely unless the
+  // ErrorThreshold is reached.
+  optional .k8s.io.apimachinery.pkg.apis.meta.v1.Duration timeout = 1;
+
+  // ErrorThreshold is the number of consecutive times the step must fail (for
+  // any reason) before retries are abandoned and the entire Promotion is marked
+  // as failed.
+  //
+  // If this field is set to 0, the effective default will be a step-specific
+  // one. If no step-specific default exists (i.e. is also 0), the effective
+  // default will be the system-wide default of 1.
+  //
+  // A value of 1 will cause the Promotion to be marked as failed after just
+  // a single failure; i.e. no retries will be attempted.
+  //
+  // There is no option to specify an infinite number of retries using a value
+  // such as -1.
+  //
+  // In a future release, Kargo is likely to become capable of distinguishing
+  // between recoverable and non-recoverable step failures. At that time, it is
+  // planned that unrecoverable failures will not be subject to this threshold
+  // and will immediately cause the Promotion to be marked as failed without
+  // further condition.
+  optional uint32 errorThreshold = 2;
 }
 
 // PromotionTemplate defines a template for a Promotion that can be used to
@@ -1097,6 +1124,30 @@ message StageStatus {
   optional PromotionReference lastPromotion = 10;
 }
 
+// StepExecutionMetadata tracks metadata pertaining to the execution of
+// a promotion step.
+message StepExecutionMetadata {
+  // Alias is the alias of the step.
+  optional string alias = 1;
+
+  // StartedAt is the time at which the first attempt to execute the step
+  // began.
+  optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time startedAt = 2;
+
+  // FinishedAt is the time at which the final attempt to execute the step
+  // completed.
+  optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time finishedAt = 3;
+
+  // ErrorCount tracks consecutive failed attempts to execute the step.
+  optional uint32 errorCount = 4;
+
+  // Status is the high-level outcome of the step.
+  optional string status = 5;
+
+  // Message is a display message about the step, including any errors.
+  optional string message = 6;
+}
+
 // Verification describes how to verify that a Promotion has been successful
 // using Argo Rollouts AnalysisTemplates.
 message Verification {
diff --git a/api/v1alpha1/promotion_types.go b/api/v1alpha1/promotion_types.go
index 7ae3a365..da18f2db 100644
--- a/api/v1alpha1/promotion_types.go
+++ b/api/v1alpha1/promotion_types.go
@@ -1,6 +1,8 @@
 package v1alpha1
 
 import (
+	"time"
+
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/yaml"
@@ -111,23 +113,58 @@ type PromotionVariable struct {
 
 // PromotionStepRetry describes the retry policy for a PromotionStep.
 type PromotionStepRetry struct {
-	// Attempts is the number of times the step can be attempted before the
-	// PromotionStep is marked as failed.
+	// Timeout is the soft maximum interval in which a step that returns a Running
+	// status (which typically indicates it's waiting for something to happen)
+	// may be retried.
+	//
+	// The maximum is a soft one because the check for whether the interval has
+	// elapsed occurs AFTER the step has run. This effectively means a step may
+	// run ONCE beyond the close of the interval.
+	//
+	// If this field is set to nil, the effective default will be a step-specific
+	// one. If no step-specific default exists (i.e. is also nil), the effective
+	// default will be the system-wide default of 0.
+	//
+	// A value of 0 will cause the step to be retried indefinitely unless the
+	// ErrorThreshold is reached.
+	Timeout *metav1.Duration `json:"timeout,omitempty" protobuf:"bytes,1,opt,name=timeout"`
+	// ErrorThreshold is the number of consecutive times the step must fail (for
+	// any reason) before retries are abandoned and the entire Promotion is marked
+	// as failed.
 	//
-	// If this field is set to 1, the step will not be retried. If this
-	// field is set to -1, the step will be retried indefinitely.
+	// If this field is set to 0, the effective default will be a step-specific
+	// one. If no step-specific default exists (i.e. is also 0), the effective
+	// default will be the system-wide default of 1.
 	//
-	// The default of this field depends on the step being executed. Refer to
-	// the documentation for the specific step for more information.
-	Attempts int64 `json:"attempts,omitempty" protobuf:"varint,1,opt,name=attempts"`
+	// A value of 1 will cause the Promotion to be marked as failed after just
+	// a single failure; i.e. no retries will be attempted.
+	//
+	// There is no option to specify an infinite number of retries using a value
+	// such as -1.
+	//
+	// In a future release, Kargo is likely to become capable of distinguishing
+	// between recoverable and non-recoverable step failures. At that time, it is
+	// planned that unrecoverable failures will not be subject to this threshold
+	// and will immediately cause the Promotion to be marked as failed without
+	// further condition.
+	ErrorThreshold uint32 `json:"errorThreshold,omitempty" protobuf:"varint,2,opt,name=errorThreshold"`
 }
 
-// GetAttempts returns the Attempts field with the given fallback value.
-func (r *PromotionStepRetry) GetAttempts(fallback int64) int64 {
-	if r == nil || r.Attempts == 0 {
+// GetTimeout returns the Timeout field with the given fallback value.
+func (r *PromotionStepRetry) GetTimeout(fallback *time.Duration) *time.Duration {
+	if r == nil || r.Timeout == nil {
 		return fallback
 	}
-	return r.Attempts
+	return &r.Timeout.Duration
+}
+
+// GetErrorThreshold returns the ErrorThreshold field with the given fallback
+// value.
+func (r *PromotionStepRetry) GetErrorThreshold(fallback uint32) uint32 {
+	if r == nil || r.ErrorThreshold == 0 {
+		return fallback
+	}
+	return r.ErrorThreshold
 }
 
 // PromotionStep describes a directive to be executed as part of a Promotion.
@@ -177,9 +214,9 @@ type PromotionStatus struct {
 	// permits steps that have already run successfully to be skipped on
 	// subsequent reconciliations attempts.
 	CurrentStep int64 `json:"currentStep,omitempty" protobuf:"varint,9,opt,name=currentStep"`
-	// CurrentStepAttempt is the number of times the current step has been
-	// attempted.
-	CurrentStepAttempt int64 `json:"currentStepAttempt,omitempty" protobuf:"varint,11,opt,name=currentStepAttempt"`
+	// StepExecutionMetadata tracks metadata pertaining to the execution
+	// of individual promotion steps.
+	StepExecutionMetadata StepExecutionMetadataList `json:"stepExecutionMetadata,omitempty" protobuf:"bytes,11,rep,name=stepExecutionMetadata"`
 	// State stores the state of the promotion process between reconciliation
 	// attempts.
 	State *apiextensionsv1.JSON `json:"state,omitempty" protobuf:"bytes,10,opt,name=state"`
@@ -224,8 +261,8 @@ func (s *HealthCheckStep) GetConfig() map[string]any {
 }
 
 // WithPhase returns a copy of PromotionStatus with the given phase
-func (p *PromotionStatus) WithPhase(phase PromotionPhase) *PromotionStatus {
-	status := p.DeepCopy()
+func (s *PromotionStatus) WithPhase(phase PromotionPhase) *PromotionStatus {
+	status := s.DeepCopy()
 	status.Phase = phase
 	return status
 }
@@ -238,3 +275,25 @@ type PromotionList struct {
 	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
 	Items           []Promotion `json:"items" protobuf:"bytes,2,rep,name=items"`
 }
+
+// StepExecutionMetadataList is a list of StepExecutionMetadata.
+type StepExecutionMetadataList []StepExecutionMetadata
+
+// StepExecutionMetadata tracks metadata pertaining to the execution of
+// a promotion step.
+type StepExecutionMetadata struct {
+	// Alias is the alias of the step.
+	Alias string `json:"alias,omitempty" protobuf:"bytes,1,opt,name=alias"`
+	// StartedAt is the time at which the first attempt to execute the step
+	// began.
+	StartedAt *metav1.Time `json:"startedAt,omitempty" protobuf:"bytes,2,opt,name=startedAt"`
+	// FinishedAt is the time at which the final attempt to execute the step
+	// completed.
+	FinishedAt *metav1.Time `json:"finishedAt,omitempty" protobuf:"bytes,3,opt,name=finishedAt"`
+	// ErrorCount tracks consecutive failed attempts to execute the step.
+	ErrorCount uint32 `json:"errorCount,omitempty" protobuf:"varint,4,opt,name=errorCount"`
+	// Status is the high-level outcome of the step.
+	Status PromotionPhase `json:"status,omitempty" protobuf:"bytes,5,opt,name=status"`
+	// Message is a display message about the step, including any errors.
+	Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"`
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index a0be333f..201bb306 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -949,6 +949,13 @@ func (in *PromotionStatus) DeepCopyInto(out *PromotionStatus) {
 		in, out := &in.FinishedAt, &out.FinishedAt
 		*out = (*in).DeepCopy()
 	}
+	if in.StepExecutionMetadata != nil {
+		in, out := &in.StepExecutionMetadata, &out.StepExecutionMetadata
+		*out = make(StepExecutionMetadataList, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 	if in.State != nil {
 		in, out := &in.State, &out.State
 		*out = new(apiextensionsv1.JSON)
@@ -972,7 +979,7 @@ func (in *PromotionStep) DeepCopyInto(out *PromotionStep) {
 	if in.Retry != nil {
 		in, out := &in.Retry, &out.Retry
 		*out = new(PromotionStepRetry)
-		**out = **in
+		(*in).DeepCopyInto(*out)
 	}
 	if in.Config != nil {
 		in, out := &in.Config, &out.Config
@@ -994,6 +1001,11 @@ func (in *PromotionStep) DeepCopy() *PromotionStep {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PromotionStepRetry) DeepCopyInto(out *PromotionStepRetry) {
 	*out = *in
+	if in.Timeout != nil {
+		in, out := &in.Timeout, &out.Timeout
+		*out = new(v1.Duration)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromotionStepRetry.
@@ -1233,6 +1245,50 @@ func (in *StageStatus) DeepCopy() *StageStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StepExecutionMetadata) DeepCopyInto(out *StepExecutionMetadata) {
+	*out = *in
+	if in.StartedAt != nil {
+		in, out := &in.StartedAt, &out.StartedAt
+		*out = (*in).DeepCopy()
+	}
+	if in.FinishedAt != nil {
+		in, out := &in.FinishedAt, &out.FinishedAt
+		*out = (*in).DeepCopy()
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepExecutionMetadata.
+func (in *StepExecutionMetadata) DeepCopy() *StepExecutionMetadata {
+	if in == nil {
+		return nil
+	}
+	out := new(StepExecutionMetadata)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in StepExecutionMetadataList) DeepCopyInto(out *StepExecutionMetadataList) {
+	{
+		in := &in
+		*out = make(StepExecutionMetadataList, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepExecutionMetadataList.
+func (in StepExecutionMetadataList) DeepCopy() StepExecutionMetadataList {
+	if in == nil {
+		return nil
+	}
+	out := new(StepExecutionMetadataList)
+	in.DeepCopyInto(out)
+	return *out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Verification) DeepCopyInto(out *Verification) {
 	*out = *in
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
index 124e0067..8c8c5321 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
@@ -98,18 +98,46 @@ spec:
                     retry:
                       description: Retry is the retry policy for this step.
                       properties:
-                        attempts:
+                        errorThreshold:
                           description: |-
-                            Attempts is the number of times the step can be attempted before the
-                            PromotionStep is marked as failed.
+                            ErrorThreshold is the number of consecutive times the step must fail (for
+                            any reason) before retries are abandoned and the entire Promotion is marked
+                            as failed.
 
-                            If this field is set to 1, the step will not be retried. If this
-                            field is set to -1, the step will be retried indefinitely.
+                            If this field is set to 0, the effective default will be a step-specific
+                            one. If no step-specific default exists (i.e. is also 0), the effective
+                            default will be the system-wide default of 1.
 
-                            The default of this field depends on the step being executed. Refer to
-                            the documentation for the specific step for more information.
-                          format: int64
+                            A value of 1 will cause the Promotion to be marked as failed after just
+                            a single failure; i.e. no retries will be attempted.
+
+                            There is no option to specify an infinite number of retries using a value
+                            such as -1.
+
+                            In a future release, Kargo is likely to become capable of distinguishing
+                            between recoverable and non-recoverable step failures. At that time, it is
+                            planned that unrecoverable failures will not be subject to this threshold
+                            and will immediately cause the Promotion to be marked as failed without
+                            further condition.
+                          format: int32
                           type: integer
+                        timeout:
+                          description: |-
+                            Timeout is the soft maximum interval in which a step that returns a Running
+                            status (which typically indicates it's waiting for something to happen)
+                            may be retried.
+
+                            The maximum is a soft one because the check for whether the interval has
+                            elapsed occurs AFTER the step has run. This effectively means a step may
+                            run ONCE beyond the close of the interval.
+
+                            If this field is set to nil, the effective default will be a step-specific
+                            one. If no step-specific default exists (i.e. is also nil), the effective
+                            default will be the system-wide default of 0.
+
+                            A value of 0 will cause the step to be retried indefinitely unless the
+                            ErrorThreshold is reached.
+                          type: string
                       type: object
                     uses:
                       description: Uses identifies a runner that can execute this
@@ -161,12 +189,6 @@ spec:
                   subsequent reconciliations attempts.
                 format: int64
                 type: integer
-              currentStepAttempt:
-                description: |-
-                  CurrentStepAttempt is the number of times the current step has been
-                  attempted.
-                format: int64
-                type: integer
               finishedAt:
                 description: FinishedAt is the time when the promotion was completed.
                 format: date-time
@@ -537,6 +559,44 @@ spec:
                   State stores the state of the promotion process between reconciliation
                   attempts.
                 x-kubernetes-preserve-unknown-fields: true
+              stepExecutionMetadata:
+                description: |-
+                  StepExecutionMetadata tracks metadata pertaining to the execution
+                  of individual promotion steps.
+                items:
+                  description: |-
+                    StepExecutionMetadata tracks metadata pertaining to the execution of
+                    a promotion step.
+                  properties:
+                    alias:
+                      description: Alias is the alias of the step.
+                      type: string
+                    errorCount:
+                      description: ErrorCount tracks consecutive failed attempts to
+                        execute the step.
+                      format: int32
+                      type: integer
+                    finishedAt:
+                      description: |-
+                        FinishedAt is the time at which the final attempt to execute the step
+                        completed.
+                      format: date-time
+                      type: string
+                    message:
+                      description: Message is a display message about the step, including
+                        any errors.
+                      type: string
+                    startedAt:
+                      description: |-
+                        StartedAt is the time at which the first attempt to execute the step
+                        began.
+                      format: date-time
+                      type: string
+                    status:
+                      description: Status is the high-level outcome of the step.
+                      type: string
+                  type: object
+                type: array
             type: object
         required:
         - spec
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
index 5e471fa9..fabd57e0 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
@@ -91,18 +91,46 @@ spec:
                             retry:
                               description: Retry is the retry policy for this step.
                               properties:
-                                attempts:
+                                errorThreshold:
                                   description: |-
-                                    Attempts is the number of times the step can be attempted before the
-                                    PromotionStep is marked as failed.
+                                    ErrorThreshold is the number of consecutive times the step must fail (for
+                                    any reason) before retries are abandoned and the entire Promotion is marked
+                                    as failed.
 
-                                    If this field is set to 1, the step will not be retried. If this
-                                    field is set to -1, the step will be retried indefinitely.
+                                    If this field is set to 0, the effective default will be a step-specific
+                                    one. If no step-specific default exists (i.e. is also 0), the effective
+                                    default will be the system-wide default of 1.
 
-                                    The default of this field depends on the step being executed. Refer to
-                                    the documentation for the specific step for more information.
-                                  format: int64
+                                    A value of 1 will cause the Promotion to be marked as failed after just
+                                    a single failure; i.e. no retries will be attempted.
+
+                                    There is no option to specify an infinite number of retries using a value
+                                    such as -1.
+
+                                    In a future release, Kargo is likely to become capable of distinguishing
+                                    between recoverable and non-recoverable step failures. At that time, it is
+                                    planned that unrecoverable failures will not be subject to this threshold
+                                    and will immediately cause the Promotion to be marked as failed without
+                                    further condition.
+                                  format: int32
                                   type: integer
+                                timeout:
+                                  description: |-
+                                    Timeout is the soft maximum interval in which a step that returns a Running
+                                    status (which typically indicates it's waiting for something to happen)
+                                    may be retried.
+
+                                    The maximum is a soft one because the check for whether the interval has
+                                    elapsed occurs AFTER the step has run. This effectively means a step may
+                                    run ONCE beyond the close of the interval.
+
+                                    If this field is set to nil, the effective default will be a step-specific
+                                    one. If no step-specific default exists (i.e. is also nil), the effective
+                                    default will be the system-wide default of 0.
+
+                                    A value of 0 will cause the step to be retried indefinitely unless the
+                                    ErrorThreshold is reached.
+                                  type: string
                               type: object
                             uses:
                               description: Uses identifies a runner that can execute
@@ -485,12 +513,6 @@ spec:
                           subsequent reconciliations attempts.
                         format: int64
                         type: integer
-                      currentStepAttempt:
-                        description: |-
-                          CurrentStepAttempt is the number of times the current step has been
-                          attempted.
-                        format: int64
-                        type: integer
                       finishedAt:
                         description: FinishedAt is the time when the promotion was
                           completed.
@@ -869,6 +891,45 @@ spec:
                           State stores the state of the promotion process between reconciliation
                           attempts.
                         x-kubernetes-preserve-unknown-fields: true
+                      stepExecutionMetadata:
+                        description: |-
+                          StepExecutionMetadata tracks metadata pertaining to the execution
+                          of individual promotion steps.
+                        items:
+                          description: |-
+                            StepExecutionMetadata tracks metadata pertaining to the execution of
+                            a promotion step.
+                          properties:
+                            alias:
+                              description: Alias is the alias of the step.
+                              type: string
+                            errorCount:
+                              description: ErrorCount tracks consecutive failed attempts
+                                to execute the step.
+                              format: int32
+                              type: integer
+                            finishedAt:
+                              description: |-
+                                FinishedAt is the time at which the final attempt to execute the step
+                                completed.
+                              format: date-time
+                              type: string
+                            message:
+                              description: Message is a display message about the
+                                step, including any errors.
+                              type: string
+                            startedAt:
+                              description: |-
+                                StartedAt is the time at which the first attempt to execute the step
+                                began.
+                              format: date-time
+                              type: string
+                            status:
+                              description: Status is the high-level outcome of the
+                                step.
+                              type: string
+                          type: object
+                        type: array
                     type: object
                 required:
                 - name
@@ -1269,12 +1330,6 @@ spec:
                           subsequent reconciliations attempts.
                         format: int64
                         type: integer
-                      currentStepAttempt:
-                        description: |-
-                          CurrentStepAttempt is the number of times the current step has been
-                          attempted.
-                        format: int64
-                        type: integer
                       finishedAt:
                         description: FinishedAt is the time when the promotion was
                           completed.
@@ -1653,6 +1708,45 @@ spec:
                           State stores the state of the promotion process between reconciliation
                           attempts.
                         x-kubernetes-preserve-unknown-fields: true
+                      stepExecutionMetadata:
+                        description: |-
+                          StepExecutionMetadata tracks metadata pertaining to the execution
+                          of individual promotion steps.
+                        items:
+                          description: |-
+                            StepExecutionMetadata tracks metadata pertaining to the execution of
+                            a promotion step.
+                          properties:
+                            alias:
+                              description: Alias is the alias of the step.
+                              type: string
+                            errorCount:
+                              description: ErrorCount tracks consecutive failed attempts
+                                to execute the step.
+                              format: int32
+                              type: integer
+                            finishedAt:
+                              description: |-
+                                FinishedAt is the time at which the final attempt to execute the step
+                                completed.
+                              format: date-time
+                              type: string
+                            message:
+                              description: Message is a display message about the
+                                step, including any errors.
+                              type: string
+                            startedAt:
+                              description: |-
+                                StartedAt is the time at which the first attempt to execute the step
+                                began.
+                              format: date-time
+                              type: string
+                            status:
+                              description: Status is the high-level outcome of the
+                                step.
+                              type: string
+                          type: object
+                        type: array
                     type: object
                 required:
                 - name
diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go
index 138cc5be..57014318 100644
--- a/internal/controller/promotions/promotions.go
+++ b/internal/controller/promotions/promotions.go
@@ -476,17 +476,17 @@ func (r *reconciler) promote(
 	}
 
 	promoCtx := directives.PromotionContext{
-		UIBaseURL:       r.cfg.APIServerBaseURL,
-		WorkDir:         filepath.Join(os.TempDir(), "promotion-"+string(workingPromo.UID)),
-		Project:         stageNamespace,
-		Stage:           stageName,
-		Promotion:       workingPromo.Name,
-		FreightRequests: stage.Spec.RequestedFreight,
-		Freight:         *workingPromo.Status.FreightCollection.DeepCopy(),
-		StartFromStep:   promo.Status.CurrentStep,
-		Attempts:        promo.Status.CurrentStepAttempt,
-		State:           directives.State(workingPromo.Status.GetState()),
-		Vars:            workingPromo.Spec.Vars,
+		UIBaseURL:             r.cfg.APIServerBaseURL,
+		WorkDir:               filepath.Join(os.TempDir(), "promotion-"+string(workingPromo.UID)),
+		Project:               stageNamespace,
+		Stage:                 stageName,
+		Promotion:             workingPromo.Name,
+		FreightRequests:       stage.Spec.RequestedFreight,
+		Freight:               *workingPromo.Status.FreightCollection.DeepCopy(),
+		StartFromStep:         promo.Status.CurrentStep,
+		StepExecutionMetadata: promo.Status.StepExecutionMetadata,
+		State:                 directives.State(workingPromo.Status.GetState()),
+		Vars:                  workingPromo.Spec.Vars,
 	}
 	if err := os.Mkdir(promoCtx.WorkDir, 0o700); err == nil {
 		// If we're working with a fresh directory, we should start the promotion
@@ -494,7 +494,8 @@ func (r *reconciler) promote(
 		// allows individual steps to self-discover that they've run before and
 		// examine the results of their own previous execution.
 		promoCtx.StartFromStep = 0
-		promoCtx.Attempts = 0
+		promoCtx.StepExecutionMetadata = nil
+		workingPromo.Status.HealthChecks = nil
 	} else if !os.IsExist(err) {
 		return nil, fmt.Errorf("error creating working directory: %w", err)
 	}
@@ -510,17 +511,16 @@ func (r *reconciler) promote(
 	workingPromo.Status.Phase = res.Status
 	workingPromo.Status.Message = res.Message
 	workingPromo.Status.CurrentStep = res.CurrentStep
-	workingPromo.Status.CurrentStepAttempt = res.Attempt
+	workingPromo.Status.StepExecutionMetadata = res.StepExecutionMetadata
 	workingPromo.Status.State = &apiextensionsv1.JSON{Raw: res.State.ToJSON()}
-	if res.Status == kargoapi.PromotionPhaseSucceeded {
-		var healthChecks []kargoapi.HealthCheckStep
-		for _, step := range res.HealthCheckSteps {
-			healthChecks = append(healthChecks, kargoapi.HealthCheckStep{
+	for _, step := range res.HealthCheckSteps {
+		workingPromo.Status.HealthChecks = append(
+			workingPromo.Status.HealthChecks,
+			kargoapi.HealthCheckStep{
 				Uses:   step.Kind,
 				Config: &apiextensionsv1.JSON{Raw: step.Config.ToJSON()},
-			})
-		}
-		workingPromo.Status.HealthChecks = healthChecks
+			},
+		)
 	}
 	if err != nil {
 		workingPromo.Status.Phase = kargoapi.PromotionPhaseErrored
diff --git a/internal/directives/argocd_updater.go b/internal/directives/argocd_updater.go
index 770e86e5..afff0328 100644
--- a/internal/directives/argocd_updater.go
+++ b/internal/directives/argocd_updater.go
@@ -11,6 +11,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -125,9 +126,14 @@ func (a *argocdUpdater) Name() string {
 	return "argocd-update"
 }
 
-// DefaultAttempts implements the RetryableStepRunner interface.
-func (a *argocdUpdater) DefaultAttempts() int64 {
-	return -1
+// DefaultTimeout implements the RetryableStepRunner interface.
+func (a *argocdUpdater) DefaultTimeout() *time.Duration {
+	return ptr.To(5 * time.Minute)
+}
+
+// DefaultErrorThreshold implements the RetryableStepRunner interface.
+func (a *argocdUpdater) DefaultErrorThreshold() uint32 {
+	return 0 // Will fall back to the system default.
 }
 
 // RunPromotionStep implements the PromotionStepRunner interface.
diff --git a/internal/directives/git_pr_waiter.go b/internal/directives/git_pr_waiter.go
index 531a13b4..748bdc58 100644
--- a/internal/directives/git_pr_waiter.go
+++ b/internal/directives/git_pr_waiter.go
@@ -38,11 +38,6 @@ func (g *gitPRWaiter) Name() string {
 	return "git-wait-for-pr"
 }
 
-// DefaultAttempts implements the RetryableStepRunner interface.
-func (g *gitPRWaiter) DefaultAttempts() int64 {
-	return -1
-}
-
 // RunPromotionStep implements the PromotionStepRunner interface.
 func (g *gitPRWaiter) RunPromotionStep(
 	ctx context.Context,
diff --git a/internal/directives/promotions.go b/internal/directives/promotions.go
index 76a68ed8..a4af12e1 100644
--- a/internal/directives/promotions.go
+++ b/internal/directives/promotions.go
@@ -3,8 +3,10 @@ package directives
 import (
 	"context"
 	"fmt"
+	"time"
 
 	"github.com/expr-lang/expr"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	yaml "sigs.k8s.io/yaml/goyaml.v3"
 
@@ -30,9 +32,12 @@ type PromotionStepRunner interface {
 // RetryableStepRunner is an interface for PromotionStepRunners that can be
 // retried in the event of a failure.
 type RetryableStepRunner interface {
-	// DefaultAttempts returns the default number of attempts the step is
-	// allowed to make before failing.
-	DefaultAttempts() int64
+	// DefaultTimeout returns the default timeout for the step.
+	DefaultTimeout() *time.Duration
+	// DefaultErrorThreshold returns the number of consecutive times the step must
+	// fail (for any reason) before retries are abandoned and the entire Promotion
+	// is marked as failed.
+	DefaultErrorThreshold() uint32
 }
 
 // PromotionContext is the context of a user-defined promotion process that is
@@ -65,9 +70,9 @@ type PromotionContext struct {
 	// StartFromStep is the index of the step from which the promotion should
 	// begin execution.
 	StartFromStep int64
-	// Attempts is the number of attempts that have been made to execute
-	// the current step.
-	Attempts int64
+	// StepExecutionMetadata tracks metadata pertaining to the execution
+	// of individual promotion steps.
+	StepExecutionMetadata kargoapi.StepExecutionMetadataList
 	// State is the current state of the promotion process.
 	State State
 	// Vars is a list of variables definitions that can be used by the
@@ -96,16 +101,29 @@ type PromotionStep struct {
 	Config []byte
 }
 
-// GetMaxAttempts returns the maximum number of attempts that can be made to
-// execute the step using the provided runner. If the runner is a
-// RetryableStepRunner, the value of its retry configuration is used as the
-// maximum default. Otherwise, the default is 1.
-func (s *PromotionStep) GetMaxAttempts(runner any) int64 {
-	fallback := int64(1)
+// GetTimeout returns the maximum interval the provided runner may spend
+// attempting to execute the step before retries are abandoned and the entire
+// Promotion is marked as failed. If the runner is a RetryableStepRunner, its
+// timeout is used as the default. Otherwise, the default is 0 (no limit).
+func (s *PromotionStep) GetTimeout(runner any) *time.Duration {
+	fallback := ptr.To(time.Duration(0))
 	if retryCfg, isRetryable := runner.(RetryableStepRunner); isRetryable {
-		fallback = retryCfg.DefaultAttempts()
+		fallback = retryCfg.DefaultTimeout()
 	}
-	return s.Retry.GetAttempts(fallback)
+	return s.Retry.GetTimeout(fallback)
+}
+
+// GetErrorThreshold returns the number of consecutive times the provided runner
+// must fail to execute the step (for any reason) before retries are abandoned
+// and the entire Promotion is marked as failed. If the runner is a
+// RetryableStepRunner, its threshold is used as the default. Otherwise, the
+// default is 1.
+func (s *PromotionStep) GetErrorThreshold(runner any) uint32 {
+	fallback := uint32(1)
+	if retryCfg, isRetryable := runner.(RetryableStepRunner); isRetryable {
+		fallback = retryCfg.DefaultErrorThreshold()
+	}
+	return s.Retry.GetErrorThreshold(fallback)
 }
 
 // GetConfig returns the Config unmarshalled into a map. Any expr-lang
@@ -214,8 +232,9 @@ type PromotionResult struct {
 	// in some external state, the value of this field will indicate where to
 	// resume the process in the next reconciliation.
 	CurrentStep int64
-	// Attempt tracks the current execution attempt of the current step.
-	Attempt int64
+	// StepExecutionMetadata tracks metadata pertaining to the execution
+	// of individual promotion steps.
+	StepExecutionMetadata kargoapi.StepExecutionMetadataList
 	// State is the current state of the promotion process.
 	State State
 }
diff --git a/internal/directives/simple_engine_promote.go b/internal/directives/simple_engine_promote.go
index cb221a04..d262e83b 100644
--- a/internal/directives/simple_engine_promote.go
+++ b/internal/directives/simple_engine_promote.go
@@ -7,6 +7,8 @@ import (
 	"strings"
 
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -55,9 +57,9 @@ func (e *SimpleEngine) executeSteps(
 	}
 
 	var (
-		healthChecks []HealthCheckStep
-		err          error
-		attempt      = promoCtx.Attempts
+		healthChecks  []HealthCheckStep
+		err           error
+		stepExecMetas = promoCtx.StepExecutionMetadata.DeepCopy()
 	)
 
 	// Execute each step in sequence, starting from the step index
@@ -66,9 +68,11 @@ func (e *SimpleEngine) executeSteps(
 		select {
 		case <-ctx.Done():
 			return PromotionResult{
-				Status:      kargoapi.PromotionPhaseErrored,
-				CurrentStep: i,
-				State:       state,
+				Status:                kargoapi.PromotionPhaseErrored,
+				CurrentStep:           i,
+				StepExecutionMetadata: stepExecMetas,
+				State:                 state,
+				HealthCheckSteps:      healthChecks,
 			}, ctx.Err()
 		default:
 		}
@@ -77,94 +81,130 @@ func (e *SimpleEngine) executeSteps(
 		step := steps[i]
 		if step.Alias, err = e.stepAlias(step.Alias, i); err != nil {
 			return PromotionResult{
-				Status:      kargoapi.PromotionPhaseErrored,
-				CurrentStep: i,
-				State:       state,
-			}, err
+				Status:                kargoapi.PromotionPhaseErrored,
+				CurrentStep:           i,
+				StepExecutionMetadata: stepExecMetas,
+				State:                 state,
+				HealthCheckSteps:      healthChecks,
+			}, fmt.Errorf("error getting step alias for step %d: %w", i, err)
 		}
 
 		// Get the PromotionStepRunner for the step.
 		reg, err := e.registry.GetPromotionStepRunnerRegistration(step.Kind)
 		if err != nil {
 			return PromotionResult{
-				Status:      kargoapi.PromotionPhaseErrored,
-				CurrentStep: i,
-				State:       state,
-			}, err
+				Status:                kargoapi.PromotionPhaseErrored,
+				CurrentStep:           i,
+				StepExecutionMetadata: stepExecMetas,
+				State:                 state,
+				HealthCheckSteps:      healthChecks,
+			}, fmt.Errorf("error getting runner for step %d: %w", i, err)
 		}
 
-		// Check if the step has exceeded the maximum number of attempts.
-		maxAttempts := step.GetMaxAttempts(reg.Runner)
-		if maxAttempts > 0 && attempt >= maxAttempts {
-			return PromotionResult{
-				Status:      kargoapi.PromotionPhaseErrored,
-				CurrentStep: i,
-				State:       state,
-				Attempt:     attempt,
-			}, fmt.Errorf("step %q exceeded max attempts", step.Alias)
+		// If we don't have metadata for this step yet, create it.
+		if int64(len(stepExecMetas)) == i {
+			stepExecMetas = append(stepExecMetas, kargoapi.StepExecutionMetadata{
+				Alias:     step.Alias,
+				StartedAt: ptr.To(metav1.Now()),
+			})
 		}
+		stepExecMeta := &stepExecMetas[i]
 
-		// Count the attempt we are about to make.
-		attempt++
-
-		// Execute the step.
+		// Execute the step
 		result, err := e.executeStep(ctx, promoCtx, step, reg, workDir, state)
+		if err != nil {
+			// Let a hard error take precedence over the result status and message.
+			stepExecMeta.Status = kargoapi.PromotionPhaseErrored
+			stepExecMeta.Message = err.Error()
+		} else {
+			stepExecMeta.Status = result.Status
+			stepExecMeta.Message = result.Message
+		}
+		state[step.Alias] = result.Output
 
-		// If the step failed, and the maximum number of attempts has not been
-		// reached, we are still "Running" the step and will retry it.
-		if err != nil || result.Status == kargoapi.PromotionPhaseErrored || result.Status == kargoapi.PromotionPhaseFailed {
-			if maxAttempts < 0 || attempt < maxAttempts {
-				var message strings.Builder
-				_, _ = message.WriteString(fmt.Sprintf("step %q failed (attempt %d)", step.Alias, attempt))
-				if result.Message != "" {
-					_, _ = message.WriteString(": ")
-					_, _ = message.WriteString(result.Message)
-				}
-				if err != nil {
-					_, _ = message.WriteString(": ")
-					_, _ = message.WriteString(err.Error())
-				}
-
-				// Update the result to indicate that the step is still running.
-				result.Status = kargoapi.PromotionPhaseRunning
-				result.Message = message.String()
-
-				// Swallow the error if the step failed, as we are still
-				// retrying it.
-				err = nil
+		if stepExecMeta.Status == kargoapi.PromotionPhaseSucceeded {
+			stepExecMeta.FinishedAt = ptr.To(metav1.Now())
+			if healthCheck := result.HealthCheckStep; healthCheck != nil {
+				healthChecks = append(healthChecks, *healthCheck)
 			}
+			continue // Move on to the next step
 		}
 
-		// Update the state with the step output, regardless of the result.
-		state[step.Alias] = result.Output
+		// Treat errors and logical failures the same for now.
+		// TODO(krancour): In the future, we should fail without retry for logical
+		// failures and unrecoverable errors and retry only those errors with a
+		// chance of recovery.
+		if stepExecMeta.Status != kargoapi.PromotionPhaseRunning {
+			stepExecMeta.ErrorCount++
+			// Check if the error threshold has been met.
+			errorThreshold := step.GetErrorThreshold(reg.Runner)
+			if stepExecMeta.ErrorCount >= errorThreshold {
+				// The error threshold has been met.
+				stepExecMeta.FinishedAt = ptr.To(metav1.Now())
+				return PromotionResult{
+						Status:                kargoapi.PromotionPhaseErrored,
+						CurrentStep:           i,
+						StepExecutionMetadata: stepExecMetas,
+						State:                 state,
+						HealthCheckSteps:      healthChecks,
+					}, fmt.Errorf(
+						"step %d met error threshold of %d: %s", i,
+						errorThreshold, stepExecMeta.Message,
+					)
+			}
+		}
 
-		// If the step was not successful, return the result to wait for
-		// a next attempt or to fail the promotion.
-		if result.Status != kargoapi.PromotionPhaseSucceeded {
+		// If we get to here, the step is either running (waiting for some external
+		// condition to be met) or it errored/failed but did not meet the error
+		// threshold. Now we need to check if the timeout has elapsed. A nil timeout
+		// or any non-positive timeout interval are treated as NO timeout, although
+		// a nil timeout really shouldn't happen.
+		timeout := step.GetTimeout(reg.Runner)
+		if timeout != nil && *timeout > 0 && metav1.Now().Sub(stepExecMeta.StartedAt.Time) > *timeout {
+			// Timeout has elapsed.
+			stepExecMeta.FinishedAt = ptr.To(metav1.Now())
 			return PromotionResult{
-				Status:      result.Status,
-				Message:     result.Message,
-				CurrentStep: i,
-				Attempt:     attempt,
-				State:       state,
-			}, err
+				Status:                kargoapi.PromotionPhaseErrored,
+				CurrentStep:           i,
+				StepExecutionMetadata: stepExecMetas,
+				State:                 state,
+				HealthCheckSteps:      healthChecks,
+			}, fmt.Errorf("step %d timeout of %s has elapsed", i, timeout.String())
 		}
 
-		// If the step was successful, reset the attempts counter and add its
-		// health check to the list.
-		attempt = 0
-		if healthCheck := result.HealthCheckStep; healthCheck != nil {
-			healthChecks = append(healthChecks, *healthCheck)
+		if stepExecMeta.Status != kargoapi.PromotionPhaseRunning {
+			// Treat the error/failure as if the step is still running so that the
+			// Promotion will be requeued. The step will be retried on the next
+			// reconciliation.
+			stepExecMeta.Message += "; step will be retried"
+			return PromotionResult{
+				Status:                kargoapi.PromotionPhaseRunning,
+				CurrentStep:           i,
+				StepExecutionMetadata: stepExecMetas,
+				State:                 state,
+				HealthCheckSteps:      healthChecks,
+			}, nil
 		}
+
+		// If we get to here, the step is still running (waiting for some external
+		// condition to be met).
+		stepExecMeta.ErrorCount = 0 // Reset the error count
+		return PromotionResult{
+			Status:                kargoapi.PromotionPhaseRunning,
+			CurrentStep:           i,
+			StepExecutionMetadata: stepExecMetas,
+			State:                 state,
+			HealthCheckSteps:      healthChecks,
+		}, nil
 	}
 
 	// All steps have succeeded, return the final state.
 	return PromotionResult{
-		Status:           kargoapi.PromotionPhaseSucceeded,
-		HealthCheckSteps: healthChecks,
-		CurrentStep:      int64(len(steps)) - 1,
-		Attempt:          0,
-		State:            state,
+		Status:                kargoapi.PromotionPhaseSucceeded,
+		CurrentStep:           int64(len(steps)) - 1,
+		StepExecutionMetadata: stepExecMetas,
+		State:                 state,
+		HealthCheckSteps:      healthChecks,
 	}, nil
 }
 
diff --git a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
index afd56da1..c28cf84a 100644
--- a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
@@ -43,12 +43,16 @@
               "retry": {
                 "description": "Retry is the retry policy for this step.",
                 "properties": {
-                  "attempts": {
-                    "description": "Attempts is the number of times the step can be attempted before the\nPromotionStep is marked as failed.\n\nIf this field is set to 1, the step will not be retried. If this\nfield is set to -1, the step will be retried indefinitely.\n\nThe default of this field depends on the step being executed. Refer to\nthe documentation for the specific step for more information.",
-                    "format": "int64",
-                    "maximum": 9223372036854776000,
-                    "minimum": -9223372036854776000,
+                  "errorThreshold": {
+                    "description": "ErrorThreshold is the number of consecutive times the step must fail (for\nany reason) before retries are abandoned and the entire Promotion is marked\nas failed.\n\nIf this field is set to 0, the effective default will be a step-specific\none. If no step-specific default exists (i.e. is also 0), the effective\ndefault will be the system-wide default of 1.\n\nA value of 1 will cause the Promotion to be marked as failed after just\na single failure; i.e. no retries will be attempted.\n\nThere is no option to specify an infinite number of retries using a value\nsuch as -1.\n\nIn a future release, Kargo is likely to become capable of distinguishing\nbetween recoverable and non-recoverable step failures. At that time, it is\nplanned that unrecoverable failures will not be subject to this threshold\nand will immediately cause the Promotion to be marked as failed without\nfurther condition.",
+                    "format": "int32",
+                    "maximum": 2147483647,
+                    "minimum": -2147483648,
                     "type": "integer"
+                  },
+                  "timeout": {
+                    "description": "Timeout is the soft maximum interval in which a step that returns a Running\nstatus (which typically indicates it's waiting for something to happen)\nmay be retried.\n\nThe maximum is a soft one because the check for whether the interval has\nelapsed occurs AFTER the step has run. This effectively means a step may\nrun ONCE beyond the close of the interval.\n\nIf this field is set to nil, the effective default will be a step-specific\none. If no step-specific default exists (i.e. is also nil), the effective\ndefault will be the system-wide default of 0.\n\nA value of 0 will cause the step to be retried indefinitely unless the\nErrorThreshold is reached.",
+                    "type": "string"
                   }
                 },
                 "type": "object"
@@ -107,13 +111,6 @@
           "minimum": -9223372036854776000,
           "type": "integer"
         },
-        "currentStepAttempt": {
-          "description": "CurrentStepAttempt is the number of times the current step has been\nattempted.",
-          "format": "int64",
-          "maximum": 9223372036854776000,
-          "minimum": -9223372036854776000,
-          "type": "integer"
-        },
         "finishedAt": {
           "description": "FinishedAt is the time when the promotion was completed.",
           "format": "date-time",
@@ -466,6 +463,45 @@
         "state": {
           "description": "State stores the state of the promotion process between reconciliation\nattempts.",
           "x-kubernetes-preserve-unknown-fields": true
+        },
+        "stepExecutionMetadata": {
+          "description": "StepExecutionMetadata tracks metadata pertaining to the execution\nof individual promotion steps.",
+          "items": {
+            "description": "StepExecutionMetadata tracks metadata pertaining to the execution of\na promotion step.",
+            "properties": {
+              "alias": {
+                "description": "Alias is the alias of the step.",
+                "type": "string"
+              },
+              "errorCount": {
+                "description": "ErrorCount tracks consecutive failed attempts to execute the step.",
+                "format": "int32",
+                "maximum": 2147483647,
+                "minimum": -2147483648,
+                "type": "integer"
+              },
+              "finishedAt": {
+                "description": "FinishedAt is the time at which the final attempt to execute the step\ncompleted.",
+                "format": "date-time",
+                "type": "string"
+              },
+              "message": {
+                "description": "Message is a display message about the step, including any errors.",
+                "type": "string"
+              },
+              "startedAt": {
+                "description": "StartedAt is the time at which the first attempt to execute the step\nbegan.",
+                "format": "date-time",
+                "type": "string"
+              },
+              "status": {
+                "description": "Status is the high-level outcome of the step.",
+                "type": "string"
+              }
+            },
+            "type": "object"
+          },
+          "type": "array"
         }
       },
       "type": "object"
diff --git a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
index c82f3a02..c912502a 100644
--- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
@@ -38,12 +38,16 @@
                       "retry": {
                         "description": "Retry is the retry policy for this step.",
                         "properties": {
-                          "attempts": {
-                            "description": "Attempts is the number of times the step can be attempted before the\nPromotionStep is marked as failed.\n\nIf this field is set to 1, the step will not be retried. If this\nfield is set to -1, the step will be retried indefinitely.\n\nThe default of this field depends on the step being executed. Refer to\nthe documentation for the specific step for more information.",
-                            "format": "int64",
-                            "maximum": 9223372036854776000,
-                            "minimum": -9223372036854776000,
+                          "errorThreshold": {
+                            "description": "ErrorThreshold is the number of consecutive times the step must fail (for\nany reason) before retries are abandoned and the entire Promotion is marked\nas failed.\n\nIf this field is set to 0, the effective default will be a step-specific\none. If no step-specific default exists (i.e. is also 0), the effective\ndefault will be the system-wide default of 1.\n\nA value of 1 will cause the Promotion to be marked as failed after just\na single failure; i.e. no retries will be attempted.\n\nThere is no option to specify an infinite number of retries using a value\nsuch as -1.\n\nIn a future release, Kargo is likely to become capable of distinguishing\nbetween recoverable and non-recoverable step failures. At that time, it is\nplanned that unrecoverable failures will not be subject to this threshold\nand will immediately cause the Promotion to be marked as failed without\nfurther condition.",
+                            "format": "int32",
+                            "maximum": 2147483647,
+                            "minimum": -2147483648,
                             "type": "integer"
+                          },
+                          "timeout": {
+                            "description": "Timeout is the soft maximum interval in which a step that returns a Running\nstatus (which typically indicates it's waiting for something to happen)\nmay be retried.\n\nThe maximum is a soft one because the check for whether the interval has\nelapsed occurs AFTER the step has run. This effectively means a step may\nrun ONCE beyond the close of the interval.\n\nIf this field is set to nil, the effective default will be a step-specific\none. If no step-specific default exists (i.e. is also nil), the effective\ndefault will be the system-wide default of 0.\n\nA value of 0 will cause the step to be retried indefinitely unless the\nErrorThreshold is reached.",
+                            "type": "string"
                           }
                         },
                         "type": "object"
@@ -425,13 +429,6 @@
                   "minimum": -9223372036854776000,
                   "type": "integer"
                 },
-                "currentStepAttempt": {
-                  "description": "CurrentStepAttempt is the number of times the current step has been\nattempted.",
-                  "format": "int64",
-                  "maximum": 9223372036854776000,
-                  "minimum": -9223372036854776000,
-                  "type": "integer"
-                },
                 "finishedAt": {
                   "description": "FinishedAt is the time when the promotion was completed.",
                   "format": "date-time",
@@ -784,6 +781,45 @@
                 "state": {
                   "description": "State stores the state of the promotion process between reconciliation\nattempts.",
                   "x-kubernetes-preserve-unknown-fields": true
+                },
+                "stepExecutionMetadata": {
+                  "description": "StepExecutionMetadata tracks metadata pertaining to the execution\nof individual promotion steps.",
+                  "items": {
+                    "description": "StepExecutionMetadata tracks metadata pertaining to the execution of\na promotion step.",
+                    "properties": {
+                      "alias": {
+                        "description": "Alias is the alias of the step.",
+                        "type": "string"
+                      },
+                      "errorCount": {
+                        "description": "ErrorCount tracks consecutive failed attempts to execute the step.",
+                        "format": "int32",
+                        "maximum": 2147483647,
+                        "minimum": -2147483648,
+                        "type": "integer"
+                      },
+                      "finishedAt": {
+                        "description": "FinishedAt is the time at which the final attempt to execute the step\ncompleted.",
+                        "format": "date-time",
+                        "type": "string"
+                      },
+                      "message": {
+                        "description": "Message is a display message about the step, including any errors.",
+                        "type": "string"
+                      },
+                      "startedAt": {
+                        "description": "StartedAt is the time at which the first attempt to execute the step\nbegan.",
+                        "format": "date-time",
+                        "type": "string"
+                      },
+                      "status": {
+                        "description": "Status is the high-level outcome of the step.",
+                        "type": "string"
+                      }
+                    },
+                    "type": "object"
+                  },
+                  "type": "array"
                 }
               },
               "type": "object"
@@ -1163,13 +1199,6 @@
                   "minimum": -9223372036854776000,
                   "type": "integer"
                 },
-                "currentStepAttempt": {
-                  "description": "CurrentStepAttempt is the number of times the current step has been\nattempted.",
-                  "format": "int64",
-                  "maximum": 9223372036854776000,
-                  "minimum": -9223372036854776000,
-                  "type": "integer"
-                },
                 "finishedAt": {
                   "description": "FinishedAt is the time when the promotion was completed.",
                   "format": "date-time",
@@ -1522,6 +1551,45 @@
                 "state": {
                   "description": "State stores the state of the promotion process between reconciliation\nattempts.",
                   "x-kubernetes-preserve-unknown-fields": true
+                },
+                "stepExecutionMetadata": {
+                  "description": "StepExecutionMetadata tracks metadata pertaining to the execution\nof individual promotion steps.",
+                  "items": {
+                    "description": "StepExecutionMetadata tracks metadata pertaining to the execution of\na promotion step.",
+                    "properties": {
+                      "alias": {
+                        "description": "Alias is the alias of the step.",
+                        "type": "string"
+                      },
+                      "errorCount": {
+                        "description": "ErrorCount tracks consecutive failed attempts to execute the step.",
+                        "format": "int32",
+                        "maximum": 2147483647,
+                        "minimum": -2147483648,
+                        "type": "integer"
+                      },
+                      "finishedAt": {
+                        "description": "FinishedAt is the time at which the final attempt to execute the step\ncompleted.",
+                        "format": "date-time",
+                        "type": "string"
+                      },
+                      "message": {
+                        "description": "Message is a display message about the step, including any errors.",
+                        "type": "string"
+                      },
+                      "startedAt": {
+                        "description": "StartedAt is the time at which the first attempt to execute the step\nbegan.",
+                        "format": "date-time",
+                        "type": "string"
+                      },
+                      "status": {
+                        "description": "Status is the high-level outcome of the step.",
+                        "type": "string"
+                      }
+                    },
+                    "type": "object"
+                  },
+                  "type": "array"
                 }
               },
               "type": "object"
diff --git a/ui/src/gen/v1alpha1/generated_pb.ts b/ui/src/gen/v1alpha1/generated_pb.ts
index 455348cf..7613e99d 100644
--- a/ui/src/gen/v1alpha1/generated_pb.ts
+++ b/ui/src/gen/v1alpha1/generated_pb.ts
@@ -2760,12 +2760,12 @@ export class PromotionStatus extends Message<PromotionStatus> {
   currentStep?: bigint;
 
   /**
-   * CurrentStepAttempt is the number of times the current step has been
-   * attempted.
+   * StepExecutionMetadata tracks metadata pertaining to the execution
+   * of individual promotion steps.
    *
-   * @generated from field: optional int64 currentStepAttempt = 11;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.StepExecutionMetadata stepExecutionMetadata = 11;
    */
-  currentStepAttempt?: bigint;
+  stepExecutionMetadata: StepExecutionMetadata[] = [];
 
   /**
    * State stores the state of the promotion process between reconciliation
@@ -2791,7 +2791,7 @@ export class PromotionStatus extends Message<PromotionStatus> {
     { no: 8, name: "healthChecks", kind: "message", T: HealthCheckStep, repeated: true },
     { no: 6, name: "finishedAt", kind: "message", T: Time, opt: true },
     { no: 9, name: "currentStep", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true },
-    { no: 11, name: "currentStepAttempt", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true },
+    { no: 11, name: "stepExecutionMetadata", kind: "message", T: StepExecutionMetadata, repeated: true },
     { no: 10, name: "state", kind: "message", T: JSON, opt: true },
   ]);
 
@@ -2889,18 +2889,49 @@ export class PromotionStep extends Message<PromotionStep> {
  */
 export class PromotionStepRetry extends Message<PromotionStepRetry> {
   /**
-   * Attempts is the number of times the step can be attempted before the
-   * PromotionStep is marked as failed.
+   * Timeout is the soft maximum interval in which a step that returns a Running
+   * status (which typically indicates it's waiting for something to happen)
+   * may be retried.
    *
-   * If this field is set to 1, the step will not be retried. If this
-   * field is set to -1, the step will be retried indefinitely.
+   * The maximum is a soft one because the check for whether the interval has
+   * elapsed occurs AFTER the step has run. This effectively means a step may
+   * run ONCE beyond the close of the interval.
    *
-   * The default of this field depends on the step being executed. Refer to
-   * the documentation for the specific step for more information.
+   * If this field is set to nil, the effective default will be a step-specific
+   * one. If no step-specific default exists (i.e. is also nil), the effective
+   * default will be the system-wide default of 0.
    *
-   * @generated from field: optional int64 attempts = 1;
+   * A value of 0 will cause the step to be retried indefinitely unless the
+   * ErrorThreshold is reached.
+   *
+   * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.Duration timeout = 1;
    */
-  attempts?: bigint;
+  timeout?: Duration;
+
+  /**
+   * ErrorThreshold is the number of consecutive times the step must fail (for
+   * any reason) before retries are abandoned and the entire Promotion is marked
+   * as failed.
+   *
+   * If this field is set to 0, the effective default will be a step-specific
+   * one. If no step-specific default exists (i.e. is also 0), the effective
+   * default will be the system-wide default of 1.
+   *
+   * A value of 1 will cause the Promotion to be marked as failed after just
+   * a single failure; i.e. no retries will be attempted.
+   *
+   * There is no option to specify an infinite number of retries using a value
+   * such as -1.
+   *
+   * In a future release, Kargo is likely to become capable of distinguishing
+   * between recoverable and non-recoverable step failures. At that time, it is
+   * planned that unrecoverable failures will not be subject to this threshold
+   * and will immediately cause the Promotion to be marked as failed without
+   * further condition.
+   *
+   * @generated from field: optional uint32 errorThreshold = 2;
+   */
+  errorThreshold?: number;
 
   constructor(data?: PartialMessage<PromotionStepRetry>) {
     super();
@@ -2910,7 +2941,8 @@ export class PromotionStepRetry extends Message<PromotionStepRetry> {
   static readonly runtime: typeof proto2 = proto2;
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.PromotionStepRetry";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
-    { no: 1, name: "attempts", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true },
+    { no: 1, name: "timeout", kind: "message", T: Duration, opt: true },
+    { no: 2, name: "errorThreshold", kind: "scalar", T: 13 /* ScalarType.UINT32 */, opt: true },
   ]);
 
   static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PromotionStepRetry {
@@ -3459,6 +3491,90 @@ export class StageStatus extends Message<StageStatus> {
   }
 }
 
+/**
+ * StepExecutionMetadata tracks metadata pertaining to the execution of
+ * a promotion step.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.StepExecutionMetadata
+ */
+export class StepExecutionMetadata extends Message<StepExecutionMetadata> {
+  /**
+   * Alias is the alias of the step.
+   *
+   * @generated from field: optional string alias = 1;
+   */
+  alias?: string;
+
+  /**
+   * StartedAt is the time at which the first attempt to execute the step
+   * began.
+   *
+   * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.Time startedAt = 2;
+   */
+  startedAt?: Time;
+
+  /**
+   * FinishedAt is the time at which the final attempt to execute the step
+   * completed.
+   *
+   * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.Time finishedAt = 3;
+   */
+  finishedAt?: Time;
+
+  /**
+   * ErrorCount tracks consecutive failed attempts to execute the step.
+   *
+   * @generated from field: optional uint32 errorCount = 4;
+   */
+  errorCount?: number;
+
+  /**
+   * Status is the high-level outcome of the step.
+   *
+   * @generated from field: optional string status = 5;
+   */
+  status?: string;
+
+  /**
+   * Message is a display message about the step, including any errors.
+   *
+   * @generated from field: optional string message = 6;
+   */
+  message?: string;
+
+  constructor(data?: PartialMessage<StepExecutionMetadata>) {
+    super();
+    proto2.util.initPartial(data, this);
+  }
+
+  static readonly runtime: typeof proto2 = proto2;
+  static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.StepExecutionMetadata";
+  static readonly fields: FieldList = proto2.util.newFieldList(() => [
+    { no: 1, name: "alias", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
+    { no: 2, name: "startedAt", kind: "message", T: Time, opt: true },
+    { no: 3, name: "finishedAt", kind: "message", T: Time, opt: true },
+    { no: 4, name: "errorCount", kind: "scalar", T: 13 /* ScalarType.UINT32 */, opt: true },
+    { no: 5, name: "status", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
+    { no: 6, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
+  ]);
+
+  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): StepExecutionMetadata {
+    return new StepExecutionMetadata().fromBinary(bytes, options);
+  }
+
+  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): StepExecutionMetadata {
+    return new StepExecutionMetadata().fromJson(jsonValue, options);
+  }
+
+  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): StepExecutionMetadata {
+    return new StepExecutionMetadata().fromJsonString(jsonString, options);
+  }
+
+  static equals(a: StepExecutionMetadata | PlainMessage<StepExecutionMetadata> | undefined, b: StepExecutionMetadata | PlainMessage<StepExecutionMetadata> | undefined): boolean {
+    return proto2.util.equals(StepExecutionMetadata, a, b);
+  }
+}
+
 /**
  * Verification describes how to verify that a Promotion has been successful
  * using Argo Rollouts AnalysisTemplates.


Test patch:
diff --git a/api/v1alpha1/promotion_types_test.go b/api/v1alpha1/promotion_types_test.go
index 98066278..c18759f2 100644
--- a/api/v1alpha1/promotion_types_test.go
+++ b/api/v1alpha1/promotion_types_test.go
@@ -2,16 +2,53 @@ package v1alpha1
 
 import (
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 )
 
-func TestPromotionRetry_GetAttempts(t *testing.T) {
+func TestPromotionRetry_GetTimeout(t *testing.T) {
 	tests := []struct {
 		name     string
 		retry    *PromotionStepRetry
-		fallback int64
-		want     int64
+		fallback *time.Duration
+		want     *time.Duration
+	}{
+		{
+			name:     "retry is nil",
+			retry:    nil,
+			fallback: ptr.To(time.Hour),
+			want:     ptr.To(time.Hour),
+		},
+		{
+			name:     "timeout is not set",
+			retry:    &PromotionStepRetry{},
+			fallback: ptr.To(time.Hour),
+			want:     ptr.To(time.Hour),
+		},
+		{
+			name: "timeout is set",
+			retry: &PromotionStepRetry{
+				Timeout: &metav1.Duration{
+					Duration: 3 * time.Hour,
+				},
+			},
+			want: ptr.To(3 * time.Hour),
+		},
+	}
+	for _, tt := range tests {
+		require.Equal(t, tt.want, tt.retry.GetTimeout(tt.fallback))
+	}
+}
+
+func TestPromotionRetry_GetErrorThreshold(t *testing.T) {
+	tests := []struct {
+		name     string
+		retry    *PromotionStepRetry
+		fallback uint32
+		want     uint32
 	}{
 		{
 			name:     "retry is nil",
@@ -20,20 +57,20 @@ func TestPromotionRetry_GetAttempts(t *testing.T) {
 			want:     1,
 		},
 		{
-			name:     "attempts is not set",
+			name:     "threshold is not set",
 			retry:    &PromotionStepRetry{},
-			fallback: -1,
-			want:     -1,
+			fallback: 1,
+			want:     1,
 		},
 		{
-			name: "attempts is set",
+			name: "threshold is set",
 			retry: &PromotionStepRetry{
-				Attempts: 3,
+				ErrorThreshold: 3,
 			},
 			want: 3,
 		},
 	}
 	for _, tt := range tests {
-		require.Equal(t, tt.want, tt.retry.GetAttempts(tt.fallback))
+		require.Equal(t, tt.want, tt.retry.GetErrorThreshold(tt.fallback))
 	}
 }
diff --git a/internal/directives/promotions_test.go b/internal/directives/promotions_test.go
index 00cea729..35871f19 100644
--- a/internal/directives/promotions_test.go
+++ b/internal/directives/promotions_test.go
@@ -3,81 +3,153 @@ package directives
 import (
 	"context"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	k8sruntime "k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 )
 
 type mockRetryableRunner struct {
-	defaultAttempts int64
+	defaultTimeout        *time.Duration
+	defaultErrorThreshold uint32
 }
 
-func (m mockRetryableRunner) DefaultAttempts() int64 {
-	return m.defaultAttempts
+func (m mockRetryableRunner) DefaultTimeout() *time.Duration {
+	return m.defaultTimeout
 }
 
-func TestPromotionStep_GetMaxAttempts(t *testing.T) {
+func (m mockRetryableRunner) DefaultErrorThreshold() uint32 {
+	return m.defaultErrorThreshold
+}
+
+func TestPromotionStep_GetTimeout(t *testing.T) {
+	tests := []struct {
+		name       string
+		step       *PromotionStep
+		runner     any
+		assertions func(t *testing.T, result *time.Duration)
+	}{
+		{
+			name: "returns 0 with no retry config",
+			step: &PromotionStep{
+				Retry: nil,
+			},
+			assertions: func(t *testing.T, result *time.Duration) {
+				assert.Equal(t, ptr.To(time.Duration(0)), result)
+			},
+		},
+		{
+			name: "returns configured timeout for non-retryable runner",
+			step: &PromotionStep{
+				Retry: &kargoapi.PromotionStepRetry{
+					Timeout: &metav1.Duration{
+						Duration: time.Duration(5),
+					},
+				},
+			},
+			runner: nil,
+			assertions: func(t *testing.T, result *time.Duration) {
+				assert.Equal(t, ptr.To(time.Duration(5)), result)
+			},
+		},
+		{
+			name: "returns configured timeout for retryable runner",
+			step: &PromotionStep{
+				Retry: &kargoapi.PromotionStepRetry{
+					Timeout: &metav1.Duration{
+						Duration: time.Duration(5),
+					},
+				},
+			},
+			runner: mockRetryableRunner{defaultTimeout: ptr.To(time.Duration(3))},
+			assertions: func(t *testing.T, result *time.Duration) {
+				assert.Equal(t, ptr.To(time.Duration(5)), result)
+			},
+		},
+		{
+			name: "returns default timeout when retry config returns nil",
+			step: &PromotionStep{
+				Retry: &kargoapi.PromotionStepRetry{},
+			},
+			runner: mockRetryableRunner{defaultTimeout: ptr.To(time.Duration(3))},
+			assertions: func(t *testing.T, result *time.Duration) {
+				assert.Equal(t, ptr.To(time.Duration(3)), result)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := tt.step.GetTimeout(tt.runner)
+			tt.assertions(t, result)
+		})
+	}
+}
+
+func TestPromotionStep_GetErrorThreshold(t *testing.T) {
 	tests := []struct {
 		name       string
 		step       *PromotionStep
 		runner     any
-		assertions func(t *testing.T, result int64)
+		assertions func(t *testing.T, result uint32)
 	}{
 		{
 			name: "returns 1 with no retry config",
 			step: &PromotionStep{
 				Retry: nil,
 			},
-			assertions: func(t *testing.T, result int64) {
-				assert.Equal(t, int64(1), result)
+			assertions: func(t *testing.T, result uint32) {
+				assert.Equal(t, uint32(1), result)
 			},
 		},
 		{
-			name: "returns configured attempts for non-retryable runner",
+			name: "returns configured threshold for non-retryable runner",
 			step: &PromotionStep{
 				Retry: &kargoapi.PromotionStepRetry{
-					Attempts: 5,
+					ErrorThreshold: 5,
 				},
 			},
 			runner: nil,
-			assertions: func(t *testing.T, result int64) {
-				assert.Equal(t, int64(5), result)
+			assertions: func(t *testing.T, result uint32) {
+				assert.Equal(t, uint32(5), result)
 			},
 		},
 		{
-			name: "returns configured attempts for retryable runner",
+			name: "returns configured threshold for retryable runner",
 			step: &PromotionStep{
 				Retry: &kargoapi.PromotionStepRetry{
-					Attempts: 5,
+					ErrorThreshold: 5,
 				},
 			},
-			runner: mockRetryableRunner{defaultAttempts: 3},
-			assertions: func(t *testing.T, result int64) {
-				assert.Equal(t, int64(5), result)
+			runner: mockRetryableRunner{defaultErrorThreshold: 3},
+			assertions: func(t *testing.T, result uint32) {
+				assert.Equal(t, uint32(5), result)
 			},
 		},
 		{
-			name: "returns default attempts when retry config returns 0",
+			name: "returns default threshold when retry config returns 0",
 			step: &PromotionStep{
 				Retry: &kargoapi.PromotionStepRetry{
-					Attempts: 0,
+					ErrorThreshold: 0,
 				},
 			},
-			runner: mockRetryableRunner{defaultAttempts: 3},
-			assertions: func(t *testing.T, result int64) {
-				assert.Equal(t, int64(3), result)
+			runner: mockRetryableRunner{defaultErrorThreshold: 3},
+			assertions: func(t *testing.T, result uint32) {
+				assert.Equal(t, uint32(3), result)
 			},
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			result := tt.step.GetMaxAttempts(tt.runner)
+			result := tt.step.GetErrorThreshold(tt.runner)
 			tt.assertions(t, result)
 		})
 	}
diff --git a/internal/directives/simple_engine_promote_test.go b/internal/directives/simple_engine_promote_test.go
index ed1a5cc4..28625a6a 100644
--- a/internal/directives/simple_engine_promote_test.go
+++ b/internal/directives/simple_engine_promote_test.go
@@ -5,10 +5,12 @@ import (
 	"errors"
 	"os"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
@@ -65,9 +67,11 @@ func TestSimpleEngine_Promote(t *testing.T) {
 				{Kind: "context-waiter"},
 			},
 			assertions: func(t *testing.T, result PromotionResult, err error) {
-				assert.Error(t, err)
+				assert.ErrorContains(t, err, "met error threshold")
 				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status)
-				assert.ErrorIs(t, err, context.Canceled)
+				assert.Len(t, result.StepExecutionMetadata, 1)
+				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.StepExecutionMetadata[0].Status)
+				assert.Contains(t, result.StepExecutionMetadata[0].Message, context.Canceled.Error())
 			},
 		},
 		{
@@ -144,6 +148,26 @@ func TestSimpleEngine_executeSteps(t *testing.T) {
 		steps      []PromotionStep
 		assertions func(*testing.T, PromotionResult, error)
 	}{
+		{
+			name: "fail on invalid step alias",
+			steps: []PromotionStep{
+				{Kind: "success-step", Alias: "step-1"},
+			},
+			assertions: func(t *testing.T, result PromotionResult, err error) {
+				assert.ErrorContains(t, err, "is forbidden")
+				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status)
+				assert.Equal(t, int64(0), result.CurrentStep)
+			},
+		},
+		{
+			name:  "runner not found",
+			steps: []PromotionStep{{Kind: "unknown-step"}},
+			assertions: func(t *testing.T, result PromotionResult, err error) {
+				assert.ErrorContains(t, err, "error getting runner for step")
+				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status)
+				assert.Equal(t, int64(0), result.CurrentStep)
+			},
+		},
 		{
 			name: "execute all steps successfully",
 			steps: []PromotionStep{
@@ -154,7 +178,14 @@ func TestSimpleEngine_executeSteps(t *testing.T) {
 				assert.NoError(t, err)
 				assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.Status)
 				assert.Equal(t, int64(1), result.CurrentStep)
-				assert.Equal(t, int64(0), result.Attempt)
+
+				// Verify the result contains metadata from both steps
+				assert.Len(t, result.StepExecutionMetadata, 2)
+				for _, metadata := range result.StepExecutionMetadata {
+					assert.Equal(t, kargoapi.PromotionPhaseSucceeded, metadata.Status)
+					assert.NotNil(t, metadata.StartedAt)
+					assert.NotNil(t, metadata.FinishedAt)
+				}
 
 				// Verify state contains outputs from both steps
 				assert.Equal(t, State{
@@ -171,16 +202,29 @@ func TestSimpleEngine_executeSteps(t *testing.T) {
 			name: "start from middle step",
 			promoCtx: PromotionContext{
 				StartFromStep: 1,
+				// Dummy metadata for the 0 step, which must have succeeded already if
+				// we're starting from step 1
+				StepExecutionMetadata: kargoapi.StepExecutionMetadataList{{}},
 			},
 			steps: []PromotionStep{
-				{Kind: "error-step", Alias: "step1"}, // This step should be skipped
+				// This step must have already succeeded and should not be run again
+				// this time.
+				{Kind: "error-step", Alias: "step1"},
+				// This step should be run
 				{Kind: "success-step", Alias: "step2"},
 			},
 			assertions: func(t *testing.T, result PromotionResult, err error) {
 				assert.NoError(t, err)
 				assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.Status)
 				assert.Equal(t, int64(1), result.CurrentStep)
-				assert.Equal(t, int64(0), result.Attempt)
+
+				// Verify the result contains metadata from both steps
+				assert.Len(t, result.StepExecutionMetadata, 2)
+				// We're not bothering with assertions on the dummy metadata for the 0
+				// step.
+				assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.StepExecutionMetadata[1].Status)
+				assert.NotNil(t, result.StepExecutionMetadata[1].StartedAt)
+				assert.NotNil(t, result.StepExecutionMetadata[1].FinishedAt)
 
 				// Verify only second step output is in state
 				assert.Equal(t, State{
@@ -191,16 +235,23 @@ func TestSimpleEngine_executeSteps(t *testing.T) {
 			},
 		},
 		{
-			name: "fail on step execution",
+			name: "error on step execution; error threshold met",
 			steps: []PromotionStep{
 				{Kind: "success-step", Alias: "step1"},
 				{Kind: "error-step", Alias: "step2"},
 			},
 			assertions: func(t *testing.T, result PromotionResult, err error) {
-				assert.ErrorContains(t, err, "something went wrong")
+				assert.ErrorContains(t, err, "met error threshold")
 				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status)
 				assert.Equal(t, int64(1), result.CurrentStep)
-				assert.Equal(t, int64(1), result.Attempt)
+				assert.Len(t, result.StepExecutionMetadata, 2)
+				assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.StepExecutionMetadata[0].Status)
+				assert.NotNil(t, result.StepExecutionMetadata[0].StartedAt)
+				assert.NotNil(t, result.StepExecutionMetadata[0].FinishedAt)
+				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.StepExecutionMetadata[1].Status)
+				assert.NotNil(t, result.StepExecutionMetadata[1].StartedAt)
+				assert.NotNil(t, result.StepExecutionMetadata[1].FinishedAt)
+				assert.Contains(t, result.StepExecutionMetadata[1].Message, "something went wrong")
 
 				// Verify first step output is preserved in state
 				assert.Equal(t, State{
@@ -212,65 +263,75 @@ func TestSimpleEngine_executeSteps(t *testing.T) {
 			},
 		},
 		{
-			name: "fail on invalid step alias",
+			name: "error on step execution; error threshold not met",
 			steps: []PromotionStep{
-				{Kind: "success-step", Alias: "step-1"},
+				{
+					Kind:  "error-step",
+					Alias: "step1",
+					Retry: &kargoapi.PromotionStepRetry{ErrorThreshold: 3},
+				},
 			},
 			assertions: func(t *testing.T, result PromotionResult, err error) {
-				assert.ErrorContains(t, err, "is forbidden")
-				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status)
+				assert.NoError(t, err)
+				assert.Equal(t, kargoapi.PromotionPhaseRunning, result.Status)
 				assert.Equal(t, int64(0), result.CurrentStep)
+				assert.Len(t, result.StepExecutionMetadata, 1)
+				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.StepExecutionMetadata[0].Status)
+				assert.NotNil(t, result.StepExecutionMetadata[0].StartedAt)
+				assert.Nil(t, result.StepExecutionMetadata[0].FinishedAt)
+				assert.Equal(t, uint32(1), result.StepExecutionMetadata[0].ErrorCount)
+				assert.Contains(t, result.StepExecutionMetadata[0].Message, "will be retried")
 			},
 		},
 		{
-			name: "context cancellation",
+			name: "step is still running; timeout elapsed",
+			promoCtx: PromotionContext{
+				StepExecutionMetadata: kargoapi.StepExecutionMetadataList{{
+					// Start time is set to an hour ago
+					StartedAt: ptr.To(metav1.NewTime(time.Now().Add(-time.Hour))),
+				}},
+			},
 			steps: []PromotionStep{
-				{Kind: "context-waiter", Alias: "step1"},
+				{
+					Kind: "running-step",
+					Retry: &kargoapi.PromotionStepRetry{
+						Timeout: &metav1.Duration{
+							Duration: time.Hour,
+						},
+					},
+				},
 			},
 			assertions: func(t *testing.T, result PromotionResult, err error) {
-				assert.ErrorIs(t, err, context.Canceled)
+				assert.ErrorContains(t, err, "timeout")
+				assert.ErrorContains(t, err, "has elapsed")
 				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status)
 				assert.Equal(t, int64(0), result.CurrentStep)
 			},
 		},
 		{
-			name: "retry failed step within max attempts",
-			promoCtx: PromotionContext{
-				Attempts: 1,
-			},
-			steps: []PromotionStep{
-				{
-					Kind:  "error-step",
-					Alias: "step1",
-					Retry: &kargoapi.PromotionStepRetry{Attempts: 3},
-				},
-			},
+			name:  "step is still running; timeout not elapsed",
+			steps: []PromotionStep{{Kind: "running-step"}},
 			assertions: func(t *testing.T, result PromotionResult, err error) {
 				assert.NoError(t, err)
 				assert.Equal(t, kargoapi.PromotionPhaseRunning, result.Status)
 				assert.Equal(t, int64(0), result.CurrentStep)
-				assert.Equal(t, int64(2), result.Attempt)
-				assert.Contains(t, result.Message, "attempt 2")
+				assert.Len(t, result.StepExecutionMetadata, 1)
+				assert.Equal(t, kargoapi.PromotionPhaseRunning, result.StepExecutionMetadata[0].Status)
+				assert.NotNil(t, result.StepExecutionMetadata[0].StartedAt)
 			},
 		},
 		{
-			name: "max attempts exceeded",
-			promoCtx: PromotionContext{
-				Attempts: 3,
-			},
+			name: "context cancellation",
 			steps: []PromotionStep{
-				{
-					Kind:  "error-step",
-					Alias: "step1",
-					Retry: &kargoapi.PromotionStepRetry{Attempts: 3},
-				},
+				{Kind: "context-waiter", Alias: "step1"},
 			},
 			assertions: func(t *testing.T, result PromotionResult, err error) {
-				assert.Error(t, err)
+				assert.ErrorContains(t, err, "met error threshold")
 				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.Status)
 				assert.Equal(t, int64(0), result.CurrentStep)
-				assert.Equal(t, int64(3), result.Attempt)
-				assert.Contains(t, err.Error(), "exceeded max attempts")
+				assert.Len(t, result.StepExecutionMetadata, 1)
+				assert.Equal(t, kargoapi.PromotionPhaseErrored, result.StepExecutionMetadata[0].Status)
+				assert.Contains(t, result.StepExecutionMetadata[0].Message, context.Canceled.Error())
 			},
 		},
 	}
@@ -291,6 +352,15 @@ func TestSimpleEngine_executeSteps(t *testing.T) {
 				},
 				&StepRunnerPermissions{},
 			)
+			testRegistry.RegisterPromotionStepRunner(
+				&mockPromotionStepRunner{
+					name: "running-step",
+					runResult: PromotionStepResult{
+						Status: kargoapi.PromotionPhaseRunning,
+					},
+				},
+				&StepRunnerPermissions{},
+			)
 			testRegistry.RegisterPromotionStepRunner(
 				&mockPromotionStepRunner{
 					name:      "error-step",