Repository: akuity/kargo
Pull request #1856 (2024-04-16 20:27:27)

refactor(controller): rework annotation mechanics
Related to: #1479 
Also addresses a bit of: #1231 (tracking who aborted a verification request)

This pull request refactors the approach to working with annotations that express desires, such as "refresh this resource" or "abort this verification." It eliminates the need to delete an annotation once a request has been fulfilled, by introducing carefully scoped predicates and comparisons against the Status of a resource to determine if a request is new.

There are multiple advantages to this approach:

1. It reduces the number of API calls during reconciliation, as we no longer need to issue a second PATCH operation to remove an annotation.
2. It removes the need to ignore update events from the PATCH operation mentioned in point 1.
3. It makes the annotations compatible with GitOps, as they can now be safely declared in the desired state (e.g., due to a YAML export at an unfortunate moment) without retriggering the request on every sync.
4. We avoid queuing for _any_ annotation change.

### Outline of changes

#### `kargo.akuity.io/refresh`

Instead of deleting the annotation at the end of the reconciliation, the token value is mirrored to the `.status.lastHandledRefresh` field of the resource. This allows the initiator of the refresh request to wait until the value in the Status field matches the token of their request or equals the current annotation value.

To enable a reconciler to detect new refresh requests, a `RefreshRequested` predicate is introduced. This predicate signals that a resource should be reconciled if the annotation is added or if the annotation value has changed compared to the previous annotation value.

#### `kargo.akuity/reverify`

Instead of having two separate annotations to put in a request to reverify the current Freight of a Stage and to keep track of the actor who put in the request. The values have been unified into the `kargo.akuity/reverify` annotation which is now expected to contain a JSON object with all information with the following structure:

```go
type VerificationRequest struct {
	// ID is the identifier of the VerificationInfo for which the request is
	// being made.
	ID string `json:"id,omitempty"`
	// Actor is the user who initiated the request.
	Actor string `json:"actor,omitempty"`
	// ControlPlane is a flag to indicate if the request has been initiated by
	// a control plane.
	ControlPlane bool `json:"controlPlane,omitempty"`
}
```

To not create a burden for e.g. `kubectl` users, a Stage can still be annotated using a simple ID string value in which case the webhook will take care of transforming this into the expected JSON payload (including information about the actor).

During reconciliation, the data from the annotation value is compared to the `.currentFreight.verficationHistory` to determine if a reverification should be performed. In addition, when it's determined that a verification attempt is due to a reverify request, the actor of this request is mirrored to the `.verificationInfo.actor` field. When a Kubernetes Event is emitted for the verification process, the value of this field is used instead of annotation data to include metadata about the actor with the Event.

To enable a reconciler to detect new reverify requests, a `ReverifyRequested` predicate is introduced. This predicate signals that a resource should be reconciled if the annotation is added or if the ID in the annotation payload has changed compared to the previous ID.


#### `kargo.akuity.io/abort`

This annotation now relies on the same mechanics as introduced for `kargo.akuity/reverify`, and allows for tracking the actor which initiated the request.

To enable a reconciler to detect new abort requests, an `AbortRequested` predicate is introduced. This predicate signals that a resource should be reconciled if the annotation is added or if the ID in the annotation payload has changed compared to the previous ID.

Only small modifications were required to the reconciliation logic itself, as the abort request was already taken into account only when the verification process is still running _and_ the value matches the ID of the current verification process. Since the former condition will be false as soon as the abort request is fulfilled, this minimized the changes to only mirroring the actor from the `VerificationRequest` to the `VerificationInfo` in the Status.

---

@devholic: specific commit of interest to you is 6cd501356dc51a3734396d6e2cd6aac5841fbf12.
@rbreeze, @rpelczar: I tried to make the frontend changes myself here... They are all related to the `kargo.akuity.io/refresh` annotation, and mainly involve waiting for the annotation value to equal the field in the Status – instead of the annotation being removed.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *docs-kargo-akuity-io* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 44f893921d3102ce1f4d0d954d66a1e92f06534a |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/docs-kargo-akuity-io/deploys/661edf314ae84a0008c074b1 |
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/akuity/kargo/pull/1856?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 `37.93103%` with `18 lines` in your changes are missing coverage. Please review.
> Project coverage is 46.18%. Comparing base [(`4ada872`)](https://app.codecov.io/gh/akuity/kargo/commit/4ada87221efc54648679d8b7dc5573306d86c335?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) to head [(`44f8939`)](https://app.codecov.io/gh/akuity/kargo/pull/1856?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).
> Report is 6 commits behind head on main.

| [Files](https://app.codecov.io/gh/akuity/kargo/pull/1856?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/1856?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% | [5 Missing and 1 partial :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/1856?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |
| [internal/cli/cmd/refresh/stage.go](https://app.codecov.io/gh/akuity/kargo/pull/1856?src=pr&el=tree&filepath=internal%2Fcli%2Fcmd%2Frefresh%2Fstage.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-aW50ZXJuYWwvY2xpL2NtZC9yZWZyZXNoL3N0YWdlLmdv) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/1856?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |
| [internal/cli/cmd/refresh/warehouse.go](https://app.codecov.io/gh/akuity/kargo/pull/1856?src=pr&el=tree&filepath=internal%2Fcli%2Fcmd%2Frefresh%2Fwarehouse.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-aW50ZXJuYWwvY2xpL2NtZC9yZWZyZXNoL3dhcmVob3VzZS5nbw==) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/1856?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |
| [internal/controller/stages/stages.go](https://app.codecov.io/gh/akuity/kargo/pull/1856?src=pr&el=tree&filepath=internal%2Fcontroller%2Fstages%2Fstages.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-aW50ZXJuYWwvY29udHJvbGxlci9zdGFnZXMvc3RhZ2VzLmdv) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/1856?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |
| [internal/controller/warehouses/warehouses.go](https://app.codecov.io/gh/akuity/kargo/pull/1856?src=pr&el=tree&filepath=internal%2Fcontroller%2Fwarehouses%2Fwarehouses.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-aW50ZXJuYWwvY29udHJvbGxlci93YXJlaG91c2VzL3dhcmVob3VzZXMuZ28=) | 0.00% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/1856?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             @@
##             main    #1856      +/-   ##
==========================================
+ Coverage   46.13%   46.18%   +0.05%     
==========================================
  Files         213      214       +1     
  Lines       13867    13857      -10     
==========================================
+ Hits         6397     6400       +3     
+ Misses       7187     7173      -14     
- Partials      283      284       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/akuity/kargo/pull/1856?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).

krancour: @hiddeco despite the size, this looks very straightforward. Love this improvement.

LGTM, but I still want @rbreeze or @rpelczar to look at the UI parts.
hiddeco: Before I merge, it would be great to have an approval from @devholic as well. Given I (carefully) reconstructed bits he has been eagerly working on, and I want to be sure I do not introduce any unforeseen side-effects. 

Patch:
diff --git a/api/v1alpha1/annotations.go b/api/v1alpha1/annotations.go
index a6e8c274..45c17be0 100644
--- a/api/v1alpha1/annotations.go
+++ b/api/v1alpha1/annotations.go
@@ -1,13 +1,27 @@
 package v1alpha1
 
-const (
-	AnnotationKeyDescription = "kargo.akuity.io/description"
+import "encoding/json"
 
+const (
+	// AnnotationKeyRefresh is an annotation key that can be set on a resource
+	// to trigger a refresh of the resource by the controller. The value of the
+	// annotation is interpreted as a token, and any change to the value of the
+	// annotation should trigger a reconciliation of the resource.
 	AnnotationKeyRefresh = "kargo.akuity.io/refresh"
 
-	AnnotationKeyReverify      = "kargo.akuity.io/reverify"
-	AnnotationKeyReverifyActor = "kargo.akuity.io/reverify-actor"
-	AnnotationKeyAbort         = "kargo.akuity.io/abort"
+	// AnnotationKeyReverify is an annotation key that can be set on a Stage
+	// resource to trigger the re-verification of its Freight. The value of the
+	// annotation should either be the ID of the verification to be reverified,
+	// or a JSON object with the structure of the VerificationRequest.
+	AnnotationKeyReverify = "kargo.akuity.io/reverify"
+
+	// AnnotationKeyAbort is an annotation key that can be set on a Stage
+	// resource to abort the verification of its Freight. The value of the
+	// annotation must be set to the identifier of the verification to be
+	// aborted.
+	AnnotationKeyAbort = "kargo.akuity.io/abort"
+
+	AnnotationKeyDescription = "kargo.akuity.io/description"
 
 	AnnotationKeyOIDCEmails   = "rbac.kargo.akuity.io/email"
 	AnnotationKeyOIDCGroups   = "rbac.kargo.akuity.io/groups"
@@ -29,3 +43,66 @@ const (
 	AnnotationKeyEventVerificationStartTime  = "event.kargo.akuity.io/verification-start-time"
 	AnnotationKeyEventVerificationFinishTime = "event.kargo.akuity.io/verification-finish-time"
 )
+
+// RefreshAnnotationValue returns the value of the AnnotationKeyRefresh
+// annotation which can be used to detect changes, and a boolean indicating
+// whether the annotation was present.
+func RefreshAnnotationValue(annotations map[string]string) (string, bool) {
+	requested, ok := annotations[AnnotationKeyRefresh]
+	return requested, ok
+}
+
+// ReverifyAnnotationValue returns the value of the AnnotationKeyReverify
+// annotation, which can be used to determine whether the verification of a
+// Freight should be rerun, and a boolean indicating whether the annotation was
+// present.
+//
+// If the value of the annotation is a valid JSON object, it is unmarshalled
+// into a VerificationRequest struct. Otherwise, the value is treated as the ID
+// of the verification to be reverified and set as the ID field of the returned
+// VerificationRequest.
+func ReverifyAnnotationValue(annotations map[string]string) (*VerificationRequest, bool) {
+	requested, ok := annotations[AnnotationKeyReverify]
+	if !ok {
+		return nil, ok
+	}
+	var vr VerificationRequest
+	if b := []byte(requested); json.Valid(b) {
+		if err := json.Unmarshal(b, &vr); err != nil {
+			return nil, false
+		}
+	} else {
+		vr.ID = requested
+	}
+	if !vr.HasID() {
+		return nil, false
+	}
+	return &vr, ok
+}
+
+// AbortAnnotationValue returns the value of the AnnotationKeyAbort annotation
+// which can be used to abort the verification of a Freight, and a boolean
+// indicating whether the annotation was present.
+//
+// If the value of the annotation is a valid JSON object, it is unmarshalled
+// into a VerificationRequest struct. Otherwise, the value is treated as the ID
+// of the verification to be aborted and set as the ID field of the returned
+// VerificationRequest.
+func AbortAnnotationValue(annotations map[string]string) (*VerificationRequest, bool) {
+	requested, ok := annotations[AnnotationKeyAbort]
+	if !ok {
+		return nil, ok
+	}
+	var vr VerificationRequest
+	if b := []byte(requested); json.Valid(b) {
+		if err := json.Unmarshal(b, &vr); err != nil {
+			return nil, false
+		}
+	} else {
+		vr.ID = requested
+	}
+	if !vr.HasID() {
+		return nil, false
+	}
+	return &vr, ok
+}
diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go
index 2af09164..914926d1 100644
--- a/api/v1alpha1/generated.pb.go
+++ b/api/v1alpha1/generated.pb.go
@@ -1811,219 +1811,222 @@ func init() {
 }
 
 var fileDescriptor_e26b7f7bbc391025 = []byte{
-	// 3380 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x1b, 0x4d, 0x6c, 0x1c, 0x57,
-	0x39, 0xb3, 0xbb, 0x5e, 0x7b, 0xbf, 0xf5, 0xef, 0xb3, 0x93, 0xba, 0x2e, 0x71, 0xa2, 0xa1, 0x54,
-	0x2d, 0x6d, 0x77, 0x49, 0x52, 0xb7, 0x69, 0x5a, 0x5a, 0x76, 0x9d, 0x3f, 0xb7, 0x4e, 0x6a, 0x9e,
-	0x9d, 0xb4, 0xa4, 0xad, 0xe0, 0x79, 0xf7, 0x79, 0x77, 0xea, 0xdd, 0x99, 0xe9, 0xcc, 0xac, 0x53,
-	0x53, 0x09, 0x28, 0x50, 0xd1, 0x0b, 0x05, 0x89, 0x4b, 0x39, 0x21, 0x01, 0x02, 0x71, 0x80, 0x1b,
-	0x42, 0x88, 0x03, 0x12, 0x15, 0x52, 0xc5, 0x01, 0x55, 0x9c, 0xca, 0x25, 0x6a, 0xcd, 0x0d, 0xa9,
-	0x70, 0xe0, 0x16, 0x09, 0x09, 0xbd, 0x9f, 0x99, 0x79, 0x33, 0x3b, 0x6b, 0xcf, 0x6c, 0x93, 0xa8,
-	0xdc, 0x76, 0xdf, 0xf7, 0xf7, 0xde, 0xf7, 0xbe, 0xff, 0x99, 0x81, 0x47, 0x5a, 0x86, 0xd7, 0xee,
-	0x6d, 0x56, 0x1a, 0x56, 0xb7, 0x4a, 0xb6, 0x7b, 0x86, 0xb7, 0x5b, 0xdd, 0x26, 0x4e, 0xcb, 0xaa,
-	0x12, 0xdb, 0xa8, 0xee, 0x9c, 0x20, 0x1d, 0xbb, 0x4d, 0x4e, 0x54, 0x5b, 0xd4, 0xa4, 0x0e, 0xf1,
-	0x68, 0xb3, 0x62, 0x3b, 0x96, 0x67, 0xa1, 0x7b, 0x43, 0xaa, 0x8a, 0xa0, 0xaa, 0x70, 0xaa, 0x0a,
-	0xb1, 0x8d, 0x8a, 0x4f, 0xb5, 0xf0, 0xb0, 0xc2, 0xbb, 0x65, 0xb5, 0xac, 0x2a, 0x27, 0xde, 0xec,
-	0x6d, 0xf1, 0x7f, 0xfc, 0x0f, 0xff, 0x25, 0x98, 0x2e, 0x3c, 0xb2, 0x7d, 0xda, 0xad, 0x18, 0x5c,
-	0x72, 0x97, 0x34, 0xda, 0x86, 0x49, 0x9d, 0xdd, 0xaa, 0xbd, 0xdd, 0x62, 0x0b, 0x6e, 0xb5, 0x4b,
-	0x3d, 0x52, 0xdd, 0xe9, 0xdb, 0xca, 0x42, 0x75, 0x10, 0x95, 0xd3, 0x33, 0x3d, 0xa3, 0x4b, 0xfb,
-	0x08, 0x1e, 0x3d, 0x88, 0xc0, 0x6d, 0xb4, 0x69, 0x97, 0xc4, 0xe9, 0xf4, 0x97, 0x60, 0xb6, 0x66,
-	0x92, 0xce, 0xae, 0x6b, 0xb8, 0xb8, 0x67, 0xd6, 0x9c, 0x56, 0xaf, 0x4b, 0x4d, 0x0f, 0x1d, 0x87,
-	0x82, 0x49, 0xba, 0x74, 0x5e, 0x3b, 0xae, 0xdd, 0x5f, 0xaa, 0x8f, 0xbf, 0x77, 0xe3, 0xd8, 0xa1,
-	0xbd, 0x1b, 0xc7, 0x0a, 0x97, 0x49, 0x97, 0x62, 0x0e, 0x41, 0x9f, 0x85, 0x91, 0x1d, 0xd2, 0xe9,
-	0xd1, 0xf9, 0x1c, 0x47, 0x99, 0x90, 0x28, 0x23, 0x57, 0xd9, 0x22, 0x16, 0x30, 0xfd, 0x3b, 0xf9,
-	0x08, 0xfb, 0x4b, 0xd4, 0x23, 0x4d, 0xe2, 0x11, 0xd4, 0x85, 0x62, 0x87, 0x6c, 0xd2, 0x8e, 0x3b,
-	0xaf, 0x1d, 0xcf, 0xdf, 0x5f, 0x3e, 0x79, 0xae, 0x92, 0x46, 0xf5, 0x95, 0x04, 0x56, 0x95, 0x55,
-	0xce, 0xe7, 0x9c, 0xe9, 0x39, 0xbb, 0xf5, 0x49, 0xb9, 0x89, 0xa2, 0x58, 0xc4, 0x52, 0x08, 0x7a,
-	0x43, 0x83, 0x32, 0x31, 0x4d, 0xcb, 0x23, 0x9e, 0x61, 0x99, 0xee, 0x7c, 0x8e, 0x0b, 0x7d, 0x66,
-	0x78, 0xa1, 0xb5, 0x90, 0x99, 0x90, 0x3c, 0x2b, 0x25, 0x97, 0x15, 0x08, 0x56, 0x65, 0x2e, 0x3c,
-	0x0e, 0x65, 0x65, 0xab, 0x68, 0x1a, 0xf2, 0xdb, 0x74, 0x57, 0xe8, 0x17, 0xb3, 0x9f, 0x68, 0x2e,
-	0xa2, 0x50, 0xa9, 0xc1, 0x33, 0xb9, 0xd3, 0xda, 0xc2, 0x53, 0x30, 0x1d, 0x17, 0x98, 0x85, 0x5e,
-	0x7f, 0x5b, 0x83, 0x39, 0xe5, 0x14, 0x98, 0x6e, 0x51, 0x87, 0x9a, 0x0d, 0x8a, 0xaa, 0x50, 0x62,
-	0x77, 0xe9, 0xda, 0xa4, 0xe1, 0x5f, 0xf5, 0x8c, 0x3c, 0x48, 0xe9, 0xb2, 0x0f, 0xc0, 0x21, 0x4e,
-	0x60, 0x16, 0xb9, 0xfd, 0xcc, 0xc2, 0x6e, 0x13, 0x97, 0xce, 0xe7, 0xa3, 0x66, 0xb1, 0xc6, 0x16,
-	0xb1, 0x80, 0xe9, 0x5f, 0x84, 0xbb, 0xfd, 0xfd, 0x6c, 0xd0, 0xae, 0xdd, 0x21, 0x1e, 0x0d, 0x37,
-	0x75, 0xa0, 0xe9, 0xe9, 0x53, 0x30, 0x51, 0xb3, 0x6d, 0xc7, 0xda, 0xa1, 0xcd, 0x75, 0x8f, 0xb4,
-	0xa8, 0xfe, 0x6d, 0x0d, 0x0e, 0xd7, 0x9c, 0x96, 0xb5, 0x7c, 0xb6, 0x66, 0xdb, 0x17, 0x29, 0xe9,
-	0x78, 0xed, 0x75, 0x8f, 0x78, 0x3d, 0x17, 0x3d, 0x05, 0x45, 0x97, 0xff, 0x92, 0xec, 0xee, 0xf3,
-	0x2d, 0x44, 0xc0, 0x6f, 0xde, 0x38, 0x36, 0x97, 0x40, 0x48, 0xb1, 0xa4, 0x42, 0x0f, 0xc0, 0x68,
-	0x97, 0xba, 0x2e, 0x69, 0xf9, 0x67, 0x9e, 0x92, 0x0c, 0x46, 0x2f, 0x89, 0x65, 0xec, 0xc3, 0xf5,
-	0xbf, 0xe4, 0x60, 0x2a, 0xe0, 0x25, 0xc5, 0xdf, 0x06, 0x05, 0xf7, 0x60, 0xbc, 0xad, 0x9c, 0x90,
-	0xeb, 0xb9, 0x7c, 0xf2, 0x89, 0x94, 0xb6, 0x9c, 0xa4, 0xa4, 0xfa, 0x9c, 0x14, 0x33, 0xae, 0xae,
-	0xe2, 0x88, 0x18, 0xd4, 0x05, 0x70, 0x77, 0xcd, 0x86, 0x14, 0x5a, 0xe0, 0x42, 0x1f, 0xcf, 0x28,
-	0x74, 0x3d, 0x60, 0x50, 0x47, 0x52, 0x24, 0x84, 0x6b, 0x58, 0x11, 0xa0, 0xff, 0x46, 0x83, 0xd9,
-	0x04, 0x3a, 0xf4, 0x64, 0xec, 0x3e, 0xef, 0xed, 0xbb, 0x4f, 0xd4, 0x47, 0x16, 0xde, 0xe6, 0x43,
-	0x30, 0xe6, 0xd0, 0x1d, 0xc3, 0x35, 0x2c, 0x53, 0x6a, 0x78, 0x5a, 0xd2, 0x8f, 0x61, 0xb9, 0x8e,
-	0x03, 0x0c, 0xf4, 0x20, 0x94, 0xfc, 0xdf, 0x4c, 0xcd, 0x79, 0x66, 0xce, 0xec, 0xe2, 0x7c, 0x54,
-	0x17, 0x87, 0x70, 0xfd, 0x63, 0x4d, 0xb9, 0xfd, 0x2b, 0x76, 0x93, 0x78, 0x94, 0x19, 0x0f, 0xb1,
-	0xed, 0xcb, 0xa1, 0x31, 0x07, 0xc6, 0x53, 0x13, 0xcb, 0xd8, 0x87, 0xa3, 0xd3, 0x30, 0x2e, 0x7f,
-	0x0a, 0x5b, 0x11, 0xbb, 0x0b, 0x2e, 0xa6, 0xa6, 0xc0, 0x70, 0x04, 0x13, 0xf5, 0x60, 0xc2, 0xb5,
-	0x7a, 0x4e, 0x83, 0x0a, 0xa1, 0x62, 0xa7, 0xe5, 0x93, 0xa7, 0xb3, 0xdc, 0xcd, 0xba, 0xc2, 0xa0,
-	0x7e, 0x58, 0x0a, 0x9d, 0x50, 0x57, 0x5d, 0x1c, 0x95, 0xa2, 0xbf, 0x0a, 0x20, 0x68, 0x2f, 0xd2,
-	0x4e, 0x17, 0x35, 0xa0, 0x68, 0x74, 0x49, 0x8b, 0xfa, 0xf1, 0x3c, 0x93, 0x39, 0x32, 0x0e, 0x2b,
-	0x8c, 0x5a, 0x6e, 0x20, 0x88, 0xe2, 0x7c, 0xd1, 0xc5, 0x92, 0xb5, 0xfe, 0x4e, 0xe0, 0xe5, 0x31,
-	0x0a, 0x16, 0x74, 0x38, 0x8e, 0x54, 0x73, 0x10, 0x74, 0x38, 0x0e, 0x16, 0x30, 0x74, 0x54, 0x44,
-	0x4c, 0xa1, 0xd9, 0xb2, 0x44, 0xc9, 0x3f, 0x4b, 0x77, 0x45, 0xf8, 0x7c, 0xc2, 0x0f, 0x9f, 0x22,
-	0x70, 0x7d, 0x2e, 0x92, 0xcf, 0x58, 0x9c, 0x50, 0x04, 0xf2, 0xb5, 0x8d, 0x5d, 0x3b, 0xc8, 0x73,
-	0xaf, 0xfb, 0x97, 0xff, 0x6c, 0xcf, 0xf5, 0xac, 0xae, 0xf1, 0x75, 0x8a, 0xda, 0x31, 0x95, 0x7c,
-	0x29, 0x8b, 0x4a, 0x02, 0x36, 0x69, 0xf4, 0xe2, 0xc0, 0xc2, 0x60, 0xaa, 0x74, 0xba, 0xa9, 0x42,
-	0xa9, 0xe7, 0xd2, 0xb3, 0x46, 0x8b, 0xba, 0x1e, 0xd7, 0xd0, 0x58, 0x18, 0xa7, 0xae, 0xf8, 0x00,
-	0x1c, 0xe2, 0xe8, 0xff, 0xcc, 0x01, 0xea, 0xb7, 0x1d, 0x66, 0xf1, 0x0e, 0xb5, 0xad, 0x2b, 0x78,
-	0x35, 0x6e, 0xf1, 0x58, 0x2c, 0x63, 0x1f, 0xce, 0xf6, 0xd5, 0x68, 0x13, 0xc7, 0x8b, 0xd7, 0x0f,
-	0xcb, 0x6c, 0x11, 0x0b, 0x18, 0x5a, 0x83, 0xb9, 0x1e, 0xe7, 0xbc, 0x41, 0x9c, 0x16, 0xf5, 0x7c,
-	0xcf, 0xe3, 0x77, 0x34, 0x56, 0xff, 0x8c, 0xa4, 0x99, 0xbb, 0x92, 0x80, 0x83, 0x13, 0x29, 0xd1,
-	0x26, 0x94, 0xb6, 0x7d, 0x35, 0xc9, 0x30, 0xb6, 0x34, 0xd4, 0xcd, 0x88, 0x58, 0x10, 0xfc, 0xc5,
-	0x21, 0x5b, 0x74, 0x19, 0x0a, 0x6d, 0xda, 0xe9, 0xce, 0x8f, 0x70, 0xf6, 0x5f, 0xc8, 0xea, 0x0b,
-	0xf5, 0x31, 0x16, 0xf2, 0xd9, 0x2f, 0xcc, 0xf9, 0xe8, 0xdf, 0x04, 0xa1, 0x95, 0x2c, 0xea, 0x3d,
-	0x38, 0x91, 0x3c, 0x00, 0xa3, 0x3b, 0xd4, 0x09, 0xd4, 0xa9, 0x30, 0xbb, 0x2a, 0x96, 0xb1, 0x0f,
-	0xd7, 0x7f, 0xa1, 0xc1, 0x0c, 0xdf, 0xc1, 0x7a, 0x6f, 0xd3, 0x6d, 0x38, 0x86, 0xcd, 0x0a, 0x91,
-	0x5b, 0xbb, 0x9b, 0xb3, 0x30, 0xed, 0xd2, 0xee, 0x0e, 0x75, 0x96, 0x2d, 0xd3, 0xf5, 0x1c, 0x62,
-	0x98, 0x9e, 0xdc, 0xd6, 0xbc, 0xc4, 0x9e, 0x5e, 0x8f, 0xc1, 0x71, 0x1f, 0x85, 0xfe, 0xf3, 0x02,
-	0x8c, 0x9e, 0x77, 0xa8, 0xd1, 0x6a, 0x7b, 0xe8, 0x6b, 0x30, 0xd6, 0x95, 0xf5, 0x1a, 0xdf, 0x1f,
-	0xbb, 0x09, 0x51, 0x24, 0x57, 0xd4, 0x22, 0xb9, 0x62, 0x6f, 0xb7, 0xd8, 0x82, 0x5b, 0x61, 0xd8,
-	0x95, 0x9d, 0x13, 0x95, 0xe7, 0x36, 0x5f, 0xa1, 0x0d, 0x8f, 0xd5, 0x7a, 0x61, 0x9a, 0x0a, 0xd7,
-	0x70, 0xc0, 0x95, 0x99, 0x30, 0xe9, 0x18, 0xc4, 0x9d, 0x1f, 0x8d, 0x9a, 0x70, 0x8d, 0x2d, 0x62,
-	0x01, 0x63, 0xae, 0x75, 0x9d, 0x38, 0xb4, 0x6d, 0xf5, 0x5c, 0x3a, 0x3f, 0x16, 0x2d, 0x01, 0x9e,
-	0xf7, 0x01, 0x38, 0xc4, 0x41, 0xd7, 0x60, 0xb4, 0x61, 0x75, 0xbb, 0x86, 0xe7, 0x87, 0xf2, 0x6a,
-	0x3a, 0x03, 0xba, 0x60, 0x78, 0xcb, 0x9c, 0x2e, 0xbc, 0x07, 0xf1, 0xdf, 0xc5, 0x3e, 0x43, 0xb4,
-	0x1e, 0x04, 0xa5, 0x02, 0x67, 0xfd, 0x60, 0x3a, 0xd6, 0x3c, 0x56, 0x0c, 0x8a, 0x3f, 0x8c, 0x29,
-	0xf7, 0x56, 0x77, 0x7e, 0x24, 0x0b, 0x53, 0x6e, 0x50, 0x21, 0x53, 0xfe, 0xd7, 0xc5, 0x92, 0x15,
-	0x7a, 0x31, 0x48, 0xf4, 0x45, 0x7e, 0x77, 0xa7, 0xd2, 0x31, 0x95, 0x97, 0x2f, 0xab, 0x8c, 0xc9,
-	0x68, 0x75, 0xe0, 0xd7, 0x01, 0xfa, 0x1f, 0x35, 0x28, 0x4b, 0xcc, 0x55, 0xc3, 0xf5, 0xd0, 0x4b,
-	0x7d, 0xa6, 0x52, 0x49, 0x67, 0x2a, 0x8c, 0x9a, 0x1b, 0x4a, 0x50, 0x47, 0xf8, 0x2b, 0x8a, 0x99,
-	0x60, 0x18, 0x31, 0x3c, 0xda, 0xf5, 0xdb, 0x8e, 0x87, 0x33, 0x9d, 0x44, 0x09, 0xd8, 0x8c, 0x07,
-	0x16, 0xac, 0xf4, 0x8f, 0x0b, 0x30, 0x2d, 0x31, 0x32, 0x54, 0xce, 0x51, 0x63, 0x2c, 0x66, 0x33,
-	0xc6, 0xdc, 0xed, 0x33, 0xc6, 0xfc, 0xed, 0x30, 0xc6, 0xc2, 0xad, 0x33, 0xc6, 0xd7, 0x60, 0x7a,
-	0x87, 0x3a, 0xc6, 0x96, 0xd1, 0xe0, 0x2d, 0xd8, 0x8a, 0xb9, 0x65, 0xc9, 0xe0, 0xfe, 0x68, 0x3a,
-	0xf6, 0x57, 0x63, 0xd4, 0xf5, 0x39, 0x16, 0xd0, 0xe2, 0xab, 0xb8, 0x4f, 0x0a, 0x7a, 0x53, 0x83,
-	0x59, 0x75, 0xf1, 0xa2, 0xe1, 0x7a, 0x96, 0xb3, 0x3b, 0x3f, 0xca, 0x0f, 0x37, 0xac, 0xf4, 0x7b,
-	0xe4, 0x39, 0x67, 0xaf, 0xf6, 0xb3, 0xc6, 0x49, 0xf2, 0xf4, 0x7f, 0xe5, 0x61, 0x22, 0xe2, 0x5b,
-	0xe8, 0x3a, 0x80, 0x40, 0xa4, 0xcd, 0x15, 0x53, 0xd6, 0x38, 0xcb, 0x43, 0x38, 0xa9, 0xdc, 0x1d,
-	0xe3, 0x22, 0x5a, 0xe9, 0x20, 0xe6, 0x86, 0x00, 0xac, 0x88, 0x42, 0xaf, 0x43, 0x99, 0xc8, 0xee,
-	0xef, 0xbc, 0xe5, 0x48, 0xb3, 0x3c, 0x3b, 0x8c, 0xe4, 0x5a, 0xc8, 0x26, 0xde, 0xc5, 0x87, 0x10,
-	0xac, 0x4a, 0x5b, 0x70, 0x60, 0x2a, 0xb6, 0xdf, 0x84, 0x4e, 0x7c, 0x45, 0xed, 0xc4, 0x53, 0x87,
-	0x2e, 0x9f, 0x2f, 0x6f, 0x69, 0xd5, 0xf6, 0xdf, 0x85, 0xe9, 0xf8, 0x4e, 0x6f, 0x99, 0xd0, 0x48,
-	0x1f, 0xad, 0xce, 0x0c, 0x7e, 0x9b, 0x83, 0x52, 0xe0, 0xc4, 0x59, 0x52, 0xfd, 0x02, 0xe4, 0x8c,
-	0xa6, 0x4c, 0xf4, 0x20, 0xb1, 0x72, 0x2b, 0x67, 0x71, 0xce, 0x68, 0xa2, 0xfb, 0xa0, 0xb8, 0xe9,
-	0x10, 0xb3, 0xd1, 0x96, 0xa9, 0x3d, 0xf0, 0xb7, 0x3a, 0x5f, 0xc5, 0x12, 0xca, 0x4a, 0x75, 0x8f,
-	0xb4, 0x78, 0x79, 0xa6, 0x94, 0xea, 0x1b, 0xa4, 0x85, 0xd9, 0x3a, 0xba, 0x00, 0x33, 0xa2, 0x37,
-	0x5d, 0x6e, 0xd3, 0xc6, 0xb6, 0xd8, 0x22, 0xf7, 0xc7, 0x52, 0xfd, 0x6e, 0x89, 0x3c, 0x73, 0x31,
-	0x8e, 0x80, 0xfb, 0x69, 0xd4, 0xee, 0xbe, 0xb8, 0x7f, 0x77, 0xcf, 0xb6, 0x4e, 0x7a, 0x5e, 0xdb,
-	0x72, 0x64, 0xb2, 0x0f, 0xb6, 0x5e, 0xe3, 0xab, 0x58, 0x42, 0xf5, 0x59, 0x98, 0xb9, 0x60, 0x78,
-	0x17, 0x7b, 0x9b, 0x6b, 0xbd, 0x4e, 0x07, 0xd3, 0x57, 0x7b, 0xac, 0x5a, 0x16, 0x8b, 0xab, 0x24,
-	0xb2, 0xf8, 0xcb, 0x11, 0x98, 0xb8, 0x60, 0x78, 0x5c, 0x81, 0x99, 0xab, 0xe7, 0x75, 0x38, 0x6c,
-	0x98, 0x2e, 0x6d, 0xf4, 0x1c, 0xba, 0xbe, 0x6d, 0xd8, 0x1b, 0xab, 0xeb, 0xdc, 0x7c, 0x76, 0x65,
-	0xf1, 0x7e, 0x54, 0x12, 0x1e, 0x5e, 0x49, 0x42, 0xc2, 0xc9, 0xb4, 0xe8, 0x24, 0x80, 0x43, 0x49,
-	0xb3, 0xae, 0x5e, 0x51, 0xe0, 0x8d, 0x38, 0x80, 0x60, 0x05, 0x0b, 0x2d, 0x41, 0xf9, 0xba, 0x63,
-	0x78, 0x54, 0x12, 0x89, 0x2b, 0x0b, 0xfc, 0xe8, 0xf9, 0x10, 0x84, 0x55, 0x3c, 0xb4, 0x03, 0x65,
-	0x3b, 0xd4, 0x85, 0x0c, 0xa6, 0x29, 0xc3, 0x87, 0xa2, 0xc4, 0x35, 0xc7, 0xea, 0x5a, 0x2c, 0x4e,
-	0x5d, 0xa2, 0x8d, 0x36, 0x31, 0x0d, 0xb7, 0x5b, 0x9f, 0x62, 0x72, 0x15, 0x14, 0xac, 0x0a, 0x42,
-	0x2d, 0x28, 0x3a, 0xd4, 0x6c, 0x52, 0x47, 0x96, 0x15, 0x29, 0x45, 0x3e, 0xcb, 0x96, 0x30, 0x27,
-	0x4c, 0x10, 0x09, 0xcc, 0x0e, 0x04, 0x14, 0x4b, 0xf6, 0xc8, 0x54, 0xfb, 0x8c, 0x51, 0x2e, 0xab,
-	0x96, 0x52, 0x96, 0x4f, 0x96, 0x20, 0x69, 0x70, 0xcf, 0x71, 0x4d, 0xf6, 0x1c, 0x63, 0x5c, 0xd4,
-	0x93, 0xe9, 0x44, 0xb1, 0x1e, 0x23, 0x41, 0x4a, 0xbc, 0xff, 0xf8, 0x53, 0x01, 0xa6, 0x2e, 0x18,
-	0x43, 0x17, 0xff, 0x1e, 0xdc, 0x25, 0x52, 0xfe, 0x3a, 0xed, 0xd0, 0x06, 0xa3, 0x5e, 0xf7, 0x1c,
-	0xe2, 0xd1, 0x96, 0xdf, 0x8c, 0x9f, 0x91, 0xa4, 0x77, 0x2d, 0x27, 0xa3, 0xdd, 0x1c, 0x0c, 0xc2,
-	0x83, 0x58, 0xa7, 0x8e, 0x35, 0x49, 0x8d, 0x47, 0x21, 0x6b, 0xe3, 0xc1, 0x0a, 0x2b, 0xd2, 0xe9,
-	0x58, 0xd7, 0x37, 0x48, 0xcb, 0x95, 0xa1, 0x28, 0x28, 0xac, 0x6a, 0x3e, 0x00, 0x87, 0x38, 0xa8,
-	0x02, 0x60, 0xb4, 0x4c, 0xcb, 0xa1, 0x9c, 0xa2, 0xc8, 0xa7, 0x4b, 0x93, 0xcc, 0xcf, 0x56, 0x82,
-	0x55, 0xac, 0x60, 0x0c, 0x76, 0xf8, 0xd1, 0x4f, 0xe0, 0xf0, 0x8f, 0xc0, 0xb8, 0x61, 0x36, 0x3a,
-	0xbd, 0x26, 0x5d, 0x23, 0x5e, 0xdb, 0x9d, 0x1f, 0xe3, 0xdb, 0x98, 0xde, 0xbb, 0x71, 0x6c, 0x7c,
-	0x45, 0x59, 0xc7, 0x11, 0x2c, 0x46, 0x45, 0x5f, 0x53, 0xa8, 0x4a, 0x21, 0xd5, 0xb9, 0xd7, 0x54,
-	0x2a, 0x15, 0x4b, 0xff, 0xab, 0x06, 0x45, 0x11, 0x94, 0xd1, 0x52, 0x6c, 0x88, 0x77, 0xb4, 0x6f,
-	0x88, 0x57, 0x4e, 0x9a, 0xc5, 0xea, 0x50, 0x34, 0x5c, 0xb7, 0x47, 0x45, 0x29, 0x5a, 0x12, 0x6e,
-	0xb7, 0xc2, 0x57, 0xb0, 0x84, 0x20, 0x03, 0x80, 0xf8, 0x53, 0x38, 0xbf, 0xae, 0x5c, 0xca, 0x3a,
-	0xa6, 0x8c, 0x8d, 0x28, 0x03, 0x80, 0x8b, 0x15, 0xe6, 0xfa, 0x4f, 0x35, 0xb8, 0x9b, 0x39, 0x09,
-	0xaf, 0x15, 0xcf, 0x52, 0x9b, 0xf9, 0xbd, 0xd9, 0xd8, 0x95, 0xb1, 0x9c, 0xc7, 0x52, 0xdb, 0x72,
-	0x0d, 0x5e, 0xae, 0x69, 0xf1, 0x58, 0xea, 0x43, 0xb0, 0x82, 0x95, 0xa2, 0x4b, 0xae, 0x42, 0x89,
-	0x97, 0xa4, 0x4c, 0xa5, 0xd2, 0xae, 0x03, 0x33, 0x5b, 0xf6, 0x01, 0x38, 0xc4, 0xd1, 0xff, 0xa6,
-	0xc1, 0xd4, 0x50, 0xd3, 0xb2, 0xa7, 0x60, 0x92, 0x17, 0x03, 0xee, 0x79, 0xa3, 0xc3, 0x6f, 0x50,
-	0xee, 0xea, 0x88, 0xc4, 0x9e, 0xbc, 0x1a, 0x81, 0xe2, 0x18, 0xb6, 0x3f, 0x6d, 0xcb, 0x1f, 0x34,
-	0x6d, 0x2b, 0x0c, 0x31, 0x6d, 0xfb, 0x50, 0x83, 0x23, 0xc9, 0xa1, 0x0b, 0xbd, 0x1c, 0x9b, 0xba,
-	0x2d, 0xa5, 0x0f, 0x84, 0x29, 0x46, 0x6d, 0x2c, 0x7d, 0xc8, 0xee, 0x42, 0x94, 0x9d, 0x4f, 0xa7,
-	0x67, 0x9f, 0x68, 0x26, 0x83, 0x3a, 0x0e, 0xfd, 0xd7, 0x1a, 0x88, 0xfb, 0xc8, 0x12, 0x68, 0x4f,
-	0x02, 0xb4, 0x64, 0x41, 0x81, 0x57, 0xe5, 0x7d, 0x05, 0x36, 0x77, 0x21, 0x80, 0x60, 0x05, 0xcb,
-	0x2f, 0xb5, 0xf2, 0x03, 0x4a, 0xad, 0xfb, 0xa0, 0xd8, 0x14, 0x53, 0xc1, 0x42, 0x34, 0x8a, 0xca,
-	0x91, 0xa0, 0x84, 0xea, 0x6f, 0x17, 0x60, 0x86, 0xef, 0x77, 0xd8, 0x24, 0x31, 0xcc, 0xde, 0x6d,
-	0x38, 0xc2, 0xef, 0xa5, 0x3f, 0xaf, 0x88, 0xe3, 0x9c, 0x96, 0xf4, 0x47, 0x56, 0x12, 0xb1, 0x6e,
-	0x0e, 0x84, 0xe0, 0x01, 0x7c, 0xff, 0x5f, 0x92, 0xc5, 0x43, 0x30, 0x66, 0x77, 0x88, 0xb7, 0x65,
-	0x39, 0x5d, 0x59, 0xae, 0x06, 0xf3, 0x89, 0x35, 0xb9, 0x8e, 0x03, 0x8c, 0xc1, 0xa9, 0x65, 0x6c,
-	0xf8, 0xd4, 0xa2, 0x9b, 0x70, 0x44, 0x29, 0x9a, 0x6e, 0xff, 0x40, 0xfa, 0x4d, 0x0d, 0x8e, 0xee,
-	0x5b, 0xa5, 0xa1, 0x66, 0x2c, 0x34, 0x3c, 0x99, 0xb9, 0xf4, 0x4b, 0x33, 0x8c, 0x7f, 0x5b, 0x83,
-	0xb9, 0xe1, 0xe7, 0xf0, 0xc7, 0xa1, 0x60, 0x87, 0xb1, 0x36, 0xc8, 0x00, 0x3c, 0xc2, 0x72, 0x48,
-	0x54, 0x31, 0xf9, 0x14, 0x8a, 0x79, 0x43, 0x83, 0x7b, 0xf6, 0x29, 0x29, 0xd1, 0x66, 0x4c, 0x2d,
-	0x67, 0x32, 0x56, 0xa9, 0x69, 0x94, 0xf2, 0xe3, 0x1c, 0x8c, 0xae, 0x39, 0xd6, 0x2b, 0xb4, 0x71,
-	0x27, 0xc6, 0xb2, 0xcf, 0x41, 0xc1, 0xb5, 0x69, 0x43, 0x36, 0xc2, 0x27, 0x52, 0x36, 0x15, 0x62,
-	0x7b, 0xeb, 0x36, 0x6d, 0x88, 0xfa, 0x97, 0xfd, 0xc2, 0x9c, 0x91, 0x32, 0x8b, 0xcc, 0x67, 0xe9,
-	0xad, 0x7d, 0x96, 0x07, 0xcf, 0x22, 0x25, 0xe6, 0xa7, 0x76, 0x16, 0x29, 0xf7, 0x37, 0x60, 0x16,
-	0xf9, 0xfd, 0xf0, 0x04, 0x4c, 0x69, 0xe8, 0x1b, 0x30, 0x63, 0xfb, 0x76, 0xb6, 0x66, 0x75, 0x8c,
-	0x86, 0x91, 0x35, 0x1d, 0xaf, 0x45, 0xc8, 0x77, 0xc3, 0xae, 0x7e, 0x2d, 0xce, 0x17, 0xf7, 0x8b,
-	0xd2, 0x2d, 0x98, 0x88, 0xa8, 0x1e, 0x9d, 0xf2, 0xdf, 0x49, 0x88, 0x96, 0x9b, 0xe2, 0x9d, 0x84,
-	0x9b, 0x37, 0x8e, 0x8d, 0x4b, 0x74, 0xf5, 0x1d, 0x85, 0x2c, 0x4f, 0xfe, 0x7f, 0x92, 0x83, 0x52,
-	0xb0, 0xb3, 0x3b, 0x60, 0xe0, 0x5f, 0x8e, 0x18, 0xf8, 0xa9, 0x8c, 0x3a, 0x4d, 0x34, 0xf1, 0x97,
-	0x63, 0x26, 0x9e, 0xf5, 0xa2, 0x0e, 0x30, 0xf2, 0x7f, 0x6b, 0xfc, 0x4e, 0x04, 0x2e, 0x1f, 0x6c,
-	0x1e, 0x3c, 0xab, 0x26, 0x30, 0xba, 0x25, 0xc6, 0x75, 0xf2, 0xa0, 0x8f, 0x66, 0x9a, 0xf1, 0x05,
-	0x63, 0xf1, 0xf0, 0xe2, 0x7c, 0x88, 0xcf, 0x17, 0x7d, 0xe5, 0xd6, 0x9c, 0x1a, 0x12, 0x4e, 0xfc,
-	0xae, 0x7a, 0xe2, 0x3b, 0xe0, 0xd8, 0x1b, 0x51, 0xc7, 0xae, 0x66, 0x3c, 0xc9, 0x00, 0xd7, 0xfe,
-	0x5e, 0x0e, 0x66, 0xfb, 0x73, 0x86, 0x8b, 0x5c, 0x98, 0x6c, 0xa9, 0xa3, 0x2b, 0xdf, 0xbf, 0x4f,
-	0xa5, 0x7e, 0x3a, 0x10, 0xd2, 0x86, 0x2d, 0x45, 0x64, 0xd9, 0xc5, 0x31, 0x11, 0xe8, 0x75, 0x98,
-	0x26, 0xd1, 0x37, 0x2c, 0xfc, 0xd3, 0x66, 0xed, 0xf0, 0xa4, 0xe0, 0xa0, 0x66, 0x8b, 0x01, 0x5c,
-	0xdc, 0x27, 0x48, 0x7f, 0x4b, 0x83, 0xa9, 0x58, 0x58, 0x62, 0x29, 0xdd, 0xf5, 0x12, 0x52, 0xba,
-	0x1c, 0xa6, 0x72, 0x18, 0x5a, 0x83, 0x39, 0xd2, 0xf3, 0xac, 0x80, 0xf6, 0x9c, 0x49, 0x36, 0x3b,
-	0xb4, 0x29, 0x8b, 0x9a, 0xe0, 0x11, 0x76, 0x2d, 0x01, 0x07, 0x27, 0x52, 0xea, 0x5f, 0x55, 0x2c,
-	0x8b, 0x07, 0xdc, 0x54, 0xfb, 0x78, 0x20, 0xea, 0x4e, 0xa5, 0xc1, 0x6e, 0xa1, 0xff, 0x2c, 0xa7,
-	0x9c, 0x55, 0xc6, 0xd0, 0xa5, 0x68, 0x0c, 0x3d, 0x16, 0x8f, 0xa1, 0x93, 0xa1, 0x72, 0x86, 0x8b,
-	0xa2, 0xe8, 0x55, 0xc5, 0x3f, 0xf2, 0x59, 0x1e, 0x27, 0xc4, 0xb6, 0x5a, 0xf1, 0xdf, 0xd2, 0x13,
-	0x33, 0xfd, 0xc0, 0x69, 0xfc, 0xe5, 0xd0, 0x69, 0x16, 0x9e, 0x80, 0x89, 0x08, 0x72, 0xa6, 0xb7,
-	0xea, 0xfe, 0xae, 0xc1, 0xd1, 0x7d, 0x47, 0x91, 0xac, 0x6e, 0x10, 0x07, 0x90, 0xfe, 0xfe, 0x58,
-	0x6a, 0xef, 0x88, 0xce, 0x8f, 0x45, 0x80, 0x11, 0xcb, 0x58, 0xb2, 0x94, 0xcc, 0x3b, 0x64, 0x53,
-	0x46, 0xc7, 0xf4, 0xcc, 0xa3, 0x73, 0xe8, 0x80, 0xf9, 0x2a, 0x11, 0xcc, 0x3b, 0x64, 0x53, 0x7f,
-	0x27, 0x07, 0xd3, 0xcc, 0xf5, 0x22, 0xdd, 0xdc, 0x1a, 0xe4, 0x5b, 0x86, 0x27, 0xcf, 0xb2, 0x94,
-	0x5a, 0x9c, 0xca, 0xa3, 0x3e, 0xca, 0xba, 0x4b, 0xe6, 0xe7, 0x8c, 0x15, 0x7a, 0xc1, 0xaf, 0x89,
-	0x33, 0x1d, 0xa1, 0xaf, 0xcf, 0xac, 0x97, 0xfa, 0x0a, 0xe9, 0x17, 0xfc, 0xb7, 0x4b, 0xf2, 0x59,
-	0x38, 0xf7, 0xbd, 0xe3, 0x20, 0x38, 0xab, 0xaf, 0xa4, 0xe8, 0x3f, 0xca, 0x81, 0x70, 0xac, 0x3b,
-	0x90, 0xe8, 0x2f, 0x45, 0x12, 0x7d, 0xca, 0x98, 0xce, 0x37, 0x97, 0x98, 0xe4, 0xe3, 0xe9, 0xee,
-	0x44, 0x16, 0x86, 0xfb, 0x27, 0xf8, 0x3f, 0x68, 0x50, 0xe2, 0x78, 0x77, 0x20, 0xd5, 0xad, 0x45,
-	0x53, 0xdd, 0x83, 0x19, 0x4e, 0x31, 0x20, 0xcd, 0xfd, 0x20, 0x2f, 0x77, 0x1f, 0x84, 0xd3, 0x36,
-	0x71, 0x9a, 0xb2, 0xb3, 0x0f, 0xc3, 0x29, 0x5b, 0xc4, 0x02, 0x86, 0x3a, 0x30, 0xe1, 0x2a, 0x86,
-	0xe2, 0xca, 0x73, 0xa6, 0x4c, 0x80, 0xaa, 0x8d, 0xb9, 0xf5, 0x19, 0xfe, 0xb6, 0x9d, 0xba, 0x84,
-	0xa3, 0xcc, 0xd1, 0x77, 0x35, 0x98, 0xb5, 0xfb, 0xf3, 0xb0, 0x34, 0x8c, 0xc7, 0x33, 0xc6, 0xc9,
-	0x90, 0x41, 0xfd, 0xae, 0xbd, 0x1b, 0xc7, 0x92, 0x32, 0x3c, 0x4e, 0x12, 0x87, 0xda, 0x30, 0xae,
-	0x3e, 0x1c, 0x96, 0x66, 0x74, 0x32, 0xfb, 0x53, 0x68, 0x31, 0x2d, 0x56, 0x57, 0x70, 0x84, 0xb3,
-	0xfe, 0xe7, 0x11, 0x28, 0x2b, 0x76, 0x87, 0x4e, 0x44, 0xd3, 0xcf, 0x3d, 0xf1, 0xf4, 0x03, 0x1c,
-	0x39, 0x92, 0x7a, 0x1c, 0x98, 0x6c, 0xf4, 0x1c, 0x87, 0x9a, 0xde, 0xf9, 0x5b, 0x52, 0x46, 0x22,
-	0x56, 0xa2, 0x2c, 0x47, 0x38, 0xe2, 0x98, 0x04, 0x56, 0xb3, 0xb6, 0xe5, 0x13, 0xfa, 0x7c, 0x96,
-	0x27, 0xf4, 0x83, 0x6b, 0x56, 0xff, 0xa9, 0xbc, 0xcf, 0x17, 0xad, 0x41, 0x51, 0x3c, 0xc8, 0x94,
-	0x8f, 0x7a, 0x1e, 0x4a, 0x3b, 0x82, 0x64, 0x34, 0x22, 0xd8, 0x8b, 0xdf, 0x58, 0xf2, 0x51, 0x73,
-	0x74, 0xe9, 0x80, 0x1c, 0xfd, 0x0c, 0x20, 0x6b, 0xd3, 0xa5, 0xce, 0x0e, 0x6d, 0x5e, 0x10, 0x1f,
-	0x12, 0x30, 0x33, 0x28, 0x1e, 0xd7, 0xee, 0xcf, 0xd7, 0x17, 0x24, 0x15, 0x7a, 0xae, 0x0f, 0x03,
-	0x27, 0x50, 0xa1, 0x1e, 0x4c, 0x4b, 0xed, 0x05, 0xf6, 0x27, 0x1f, 0x94, 0x65, 0xed, 0x68, 0xc2,
-	0x37, 0x2a, 0x96, 0x63, 0x0c, 0x71, 0x9f, 0x08, 0xe6, 0xb8, 0x1d, 0xa2, 0xa4, 0xeb, 0x79, 0x18,
-	0x5e, 0x26, 0x77, 0xdc, 0x55, 0x95, 0x1b, 0x8e, 0x32, 0xd7, 0x97, 0x60, 0x46, 0x98, 0xb1, 0x9a,
-	0x48, 0x0f, 0x7e, 0xc3, 0xfd, 0xf7, 0x1a, 0x44, 0x03, 0x42, 0xf4, 0xcd, 0x1d, 0x2d, 0xc5, 0x9b,
-	0x3b, 0xd7, 0x61, 0xb2, 0x67, 0xbb, 0x9e, 0x43, 0x49, 0x97, 0xef, 0xc0, 0x0f, 0x97, 0x8f, 0x65,
-	0x09, 0xfa, 0x6a, 0x2a, 0x0c, 0xca, 0xf4, 0x2b, 0x11, 0xb6, 0x38, 0x26, 0x46, 0xff, 0x6f, 0x0e,
-	0x22, 0x9e, 0x8d, 0xde, 0xd2, 0x60, 0x86, 0xc4, 0x5e, 0xf7, 0xf7, 0x1b, 0x86, 0xa7, 0xb3, 0x7d,
-	0x83, 0xd1, 0xf7, 0xb5, 0x40, 0x38, 0x1a, 0x88, 0xa3, 0xb8, 0xb8, 0x5f, 0x28, 0x8f, 0xa3, 0xa4,
-	0xff, 0x7b, 0x8e, 0x6c, 0x71, 0x34, 0xe1, 0x83, 0x10, 0x11, 0x47, 0x13, 0x00, 0x38, 0x49, 0x1c,
-	0x7a, 0x11, 0x0a, 0xc4, 0x69, 0xf9, 0xcf, 0xa7, 0xb2, 0x8b, 0xf5, 0x3f, 0xd3, 0x09, 0x6d, 0xa7,
-	0xe6, 0xb4, 0x5c, 0xcc, 0x99, 0xea, 0xbf, 0xcb, 0x43, 0xdf, 0x9b, 0x45, 0xf2, 0xad, 0x8c, 0x42,
-	0xe2, 0x5b, 0x19, 0xcf, 0x43, 0xc9, 0xf5, 0x88, 0xe3, 0x6d, 0x18, 0x5d, 0x2a, 0x9f, 0xc4, 0x7f,
-	0x3e, 0x5d, 0xba, 0x66, 0x14, 0xe2, 0x99, 0xf4, 0xba, 0xcf, 0x00, 0x87, 0xbc, 0xd0, 0xe9, 0x68,
-	0xd0, 0xd6, 0xe3, 0x41, 0x7b, 0x46, 0xdd, 0xe6, 0xb0, 0x6d, 0x43, 0x17, 0xca, 0x8a, 0x8a, 0x65,
-	0x4a, 0x3a, 0x93, 0x59, 0xa5, 0x4a, 0xe8, 0x15, 0x9f, 0xf1, 0x84, 0x10, 0x95, 0x3f, 0xba, 0x06,
-	0xb0, 0x65, 0x98, 0x86, 0xdb, 0xe6, 0xda, 0x2a, 0x66, 0xd6, 0x16, 0x1f, 0xdb, 0x9f, 0x0f, 0x38,
-	0x60, 0x85, 0x9b, 0x3e, 0x05, 0x13, 0x91, 0x97, 0x80, 0xf8, 0x60, 0x29, 0x70, 0xee, 0x4f, 0xeb,
-	0x60, 0x29, 0xd8, 0xe0, 0xad, 0x1c, 0x2c, 0x85, 0x4c, 0xf7, 0xaf, 0x3b, 0xdf, 0xd5, 0x60, 0x22,
-	0xc0, 0xfd, 0xd4, 0x8e, 0x59, 0x82, 0x1d, 0x0e, 0xa8, 0x3f, 0x7f, 0xa5, 0x9e, 0x22, 0x5a, 0x83,
-	0xe6, 0xf6, 0xa9, 0x41, 0xdd, 0xfe, 0x1a, 0x34, 0x43, 0xcd, 0x11, 0xef, 0xef, 0x94, 0x0f, 0x3f,
-	0xf6, 0x29, 0x45, 0xf5, 0xff, 0x68, 0x30, 0x15, 0xbb, 0x1d, 0xd5, 0x5d, 0xf3, 0x43, 0x55, 0x10,
-	0x85, 0xa1, 0x2a, 0x08, 0x03, 0xca, 0x2c, 0xdb, 0xfa, 0xe5, 0xdd, 0xc8, 0x27, 0x9b, 0x12, 0x32,
-	0xb7, 0x5f, 0x0d, 0xd9, 0x61, 0x95, 0x77, 0xfd, 0x99, 0xf7, 0x3e, 0x5a, 0x3c, 0xf4, 0xfe, 0x47,
-	0x8b, 0x87, 0x3e, 0xf8, 0x68, 0xf1, 0xd0, 0xb7, 0xf6, 0x16, 0xb5, 0xf7, 0xf6, 0x16, 0xb5, 0xf7,
-	0xf7, 0x16, 0xb5, 0x0f, 0xf6, 0x16, 0xb5, 0x0f, 0xf7, 0x16, 0xb5, 0x1f, 0xfe, 0x63, 0xf1, 0xd0,
-	0xb5, 0x7b, 0xd3, 0x7c, 0x76, 0xfa, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x33, 0x3b, 0x04, 0xe2,
-	0x9d, 0x3a, 0x00, 0x00,
+	// 3439 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5b, 0x4d, 0x6c, 0x24, 0xd5,
+	0xb5, 0x9e, 0xea, 0x6e, 0xb7, 0xdd, 0xa7, 0xfd, 0x7b, 0xed, 0x19, 0x8c, 0x79, 0xe3, 0x19, 0x15,
+	0x3c, 0x04, 0x0f, 0x68, 0xbf, 0x99, 0xc1, 0x30, 0x0c, 0x3c, 0x78, 0x6e, 0xcf, 0x9f, 0x07, 0xcf,
+	0xe0, 0x5c, 0x7b, 0x06, 0x32, 0x80, 0x92, 0xeb, 0xee, 0xeb, 0xee, 0xc2, 0xdd, 0x5d, 0x45, 0xdd,
+	0x6a, 0x0f, 0x0e, 0x52, 0x12, 0x92, 0xa0, 0xb0, 0x09, 0x49, 0x94, 0x0d, 0x59, 0x45, 0x8a, 0xa2,
+	0x44, 0x59, 0x24, 0xbb, 0x2c, 0xa2, 0x2c, 0x90, 0xc2, 0x06, 0x65, 0x11, 0xa1, 0xac, 0x88, 0x14,
+	0x8d, 0xc0, 0xd9, 0x45, 0x22, 0xec, 0x47, 0x8a, 0x14, 0xdd, 0x9f, 0xaa, 0xba, 0x55, 0x5d, 0x6d,
+	0x57, 0x35, 0x33, 0x23, 0xb2, 0xb3, 0xef, 0x39, 0xe7, 0x3b, 0xf7, 0xe7, 0xdc, 0xf3, 0x57, 0xb7,
+	0xe1, 0xf1, 0x86, 0xe5, 0x35, 0xbb, 0x9b, 0x95, 0x9a, 0xdd, 0x5e, 0x20, 0xdb, 0x5d, 0xcb, 0xdb,
+	0x5d, 0xd8, 0x26, 0x6e, 0xc3, 0x5e, 0x20, 0x8e, 0xb5, 0xb0, 0x73, 0x82, 0xb4, 0x9c, 0x26, 0x39,
+	0xb1, 0xd0, 0xa0, 0x1d, 0xea, 0x12, 0x8f, 0xd6, 0x2b, 0x8e, 0x6b, 0x7b, 0x36, 0x7a, 0x20, 0x94,
+	0xaa, 0x48, 0xa9, 0x8a, 0x90, 0xaa, 0x10, 0xc7, 0xaa, 0xf8, 0x52, 0x73, 0x8f, 0x69, 0xd8, 0x0d,
+	0xbb, 0x61, 0x2f, 0x08, 0xe1, 0xcd, 0xee, 0x96, 0xf8, 0x4f, 0xfc, 0x23, 0xfe, 0x92, 0xa0, 0x73,
+	0x8f, 0x6f, 0x9f, 0x66, 0x15, 0x4b, 0x68, 0x6e, 0x93, 0x5a, 0xd3, 0xea, 0x50, 0x77, 0x77, 0xc1,
+	0xd9, 0x6e, 0xf0, 0x01, 0xb6, 0xd0, 0xa6, 0x1e, 0x59, 0xd8, 0xe9, 0x99, 0xca, 0xdc, 0x42, 0x3f,
+	0x29, 0xb7, 0xdb, 0xf1, 0xac, 0x36, 0xed, 0x11, 0x78, 0xe2, 0x20, 0x01, 0x56, 0x6b, 0xd2, 0x36,
+	0x89, 0xcb, 0x99, 0xaf, 0xc0, 0xf4, 0x52, 0x87, 0xb4, 0x76, 0x99, 0xc5, 0x70, 0xb7, 0xb3, 0xe4,
+	0x36, 0xba, 0x6d, 0xda, 0xf1, 0xd0, 0x71, 0x28, 0x74, 0x48, 0x9b, 0xce, 0x1a, 0xc7, 0x8d, 0x87,
+	0x4a, 0xd5, 0xd1, 0x0f, 0x6f, 0x1e, 0x3b, 0xb4, 0x77, 0xf3, 0x58, 0xe1, 0x0a, 0x69, 0x53, 0x2c,
+	0x28, 0xe8, 0x7e, 0x18, 0xda, 0x21, 0xad, 0x2e, 0x9d, 0xcd, 0x09, 0x96, 0x31, 0xc5, 0x32, 0x74,
+	0x8d, 0x0f, 0x62, 0x49, 0x33, 0xbf, 0x9b, 0x8f, 0xc0, 0x5f, 0xa6, 0x1e, 0xa9, 0x13, 0x8f, 0xa0,
+	0x36, 0x14, 0x5b, 0x64, 0x93, 0xb6, 0xd8, 0xac, 0x71, 0x3c, 0xff, 0x50, 0xf9, 0xe4, 0xb9, 0x4a,
+	0x9a, 0xad, 0xaf, 0x24, 0x40, 0x55, 0x56, 0x05, 0xce, 0xb9, 0x8e, 0xe7, 0xee, 0x56, 0xc7, 0xd5,
+	0x24, 0x8a, 0x72, 0x10, 0x2b, 0x25, 0xe8, 0x2d, 0x03, 0xca, 0xa4, 0xd3, 0xb1, 0x3d, 0xe2, 0x59,
+	0x76, 0x87, 0xcd, 0xe6, 0x84, 0xd2, 0x4b, 0x83, 0x2b, 0x5d, 0x0a, 0xc1, 0xa4, 0xe6, 0x69, 0xa5,
+	0xb9, 0xac, 0x51, 0xb0, 0xae, 0x73, 0xee, 0x29, 0x28, 0x6b, 0x53, 0x45, 0x93, 0x90, 0xdf, 0xa6,
+	0xbb, 0x72, 0x7f, 0x31, 0xff, 0x13, 0xcd, 0x44, 0x36, 0x54, 0xed, 0xe0, 0x99, 0xdc, 0x69, 0x63,
+	0xee, 0x59, 0x98, 0x8c, 0x2b, 0xcc, 0x22, 0x6f, 0xbe, 0x6b, 0xc0, 0x8c, 0xb6, 0x0a, 0x4c, 0xb7,
+	0xa8, 0x4b, 0x3b, 0x35, 0x8a, 0x16, 0xa0, 0xc4, 0xcf, 0x92, 0x39, 0xa4, 0xe6, 0x1f, 0xf5, 0x94,
+	0x5a, 0x48, 0xe9, 0x8a, 0x4f, 0xc0, 0x21, 0x4f, 0x60, 0x16, 0xb9, 0xfd, 0xcc, 0xc2, 0x69, 0x12,
+	0x46, 0x67, 0xf3, 0x51, 0xb3, 0x58, 0xe3, 0x83, 0x58, 0xd2, 0xcc, 0xff, 0x83, 0x7b, 0xfd, 0xf9,
+	0x6c, 0xd0, 0xb6, 0xd3, 0x22, 0x1e, 0x0d, 0x27, 0x75, 0xa0, 0xe9, 0x99, 0x13, 0x30, 0xb6, 0xe4,
+	0x38, 0xae, 0xbd, 0x43, 0xeb, 0xeb, 0x1e, 0x69, 0x50, 0xf3, 0x3b, 0x06, 0x1c, 0x5e, 0x72, 0x1b,
+	0xf6, 0xf2, 0xd9, 0x25, 0xc7, 0xb9, 0x48, 0x49, 0xcb, 0x6b, 0xae, 0x7b, 0xc4, 0xeb, 0x32, 0xf4,
+	0x2c, 0x14, 0x99, 0xf8, 0x4b, 0xc1, 0x3d, 0xe8, 0x5b, 0x88, 0xa4, 0xdf, 0xba, 0x79, 0x6c, 0x26,
+	0x41, 0x90, 0x62, 0x25, 0x85, 0x1e, 0x86, 0xe1, 0x36, 0x65, 0x8c, 0x34, 0xfc, 0x35, 0x4f, 0x28,
+	0x80, 0xe1, 0xcb, 0x72, 0x18, 0xfb, 0x74, 0xf3, 0x4f, 0x39, 0x98, 0x08, 0xb0, 0x94, 0xfa, 0x3b,
+	0xb0, 0xc1, 0x5d, 0x18, 0x6d, 0x6a, 0x2b, 0x14, 0xfb, 0x5c, 0x3e, 0xf9, 0x74, 0x4a, 0x5b, 0x4e,
+	0xda, 0xa4, 0xea, 0x8c, 0x52, 0x33, 0xaa, 0x8f, 0xe2, 0x88, 0x1a, 0xd4, 0x06, 0x60, 0xbb, 0x9d,
+	0x9a, 0x52, 0x5a, 0x10, 0x4a, 0x9f, 0xca, 0xa8, 0x74, 0x3d, 0x00, 0xa8, 0x22, 0xa5, 0x12, 0xc2,
+	0x31, 0xac, 0x29, 0x30, 0x7f, 0x6b, 0xc0, 0x74, 0x82, 0x1c, 0x7a, 0x26, 0x76, 0x9e, 0x0f, 0xf4,
+	0x9c, 0x27, 0xea, 0x11, 0x0b, 0x4f, 0xf3, 0x51, 0x18, 0x71, 0xe9, 0x8e, 0xc5, 0x2c, 0xbb, 0xa3,
+	0x76, 0x78, 0x52, 0xc9, 0x8f, 0x60, 0x35, 0x8e, 0x03, 0x0e, 0xf4, 0x08, 0x94, 0xfc, 0xbf, 0xf9,
+	0x36, 0xe7, 0xb9, 0x39, 0xf3, 0x83, 0xf3, 0x59, 0x19, 0x0e, 0xe9, 0xe6, 0x67, 0x86, 0x76, 0xfa,
+	0x57, 0x9d, 0x3a, 0xf1, 0x28, 0x37, 0x1e, 0xe2, 0x38, 0x57, 0x42, 0x63, 0x0e, 0x8c, 0x67, 0x49,
+	0x0e, 0x63, 0x9f, 0x8e, 0x4e, 0xc3, 0xa8, 0xfa, 0x53, 0xda, 0x8a, 0x9c, 0x5d, 0x70, 0x30, 0x4b,
+	0x1a, 0x0d, 0x47, 0x38, 0x51, 0x17, 0xc6, 0x98, 0xdd, 0x75, 0x6b, 0x54, 0x2a, 0x95, 0x33, 0x2d,
+	0x9f, 0x3c, 0x9d, 0xe5, 0x6c, 0xd6, 0x35, 0x80, 0xea, 0x61, 0xa5, 0x74, 0x4c, 0x1f, 0x65, 0x38,
+	0xaa, 0xc5, 0x7c, 0x1d, 0x40, 0xca, 0x5e, 0xa4, 0xad, 0x36, 0xaa, 0x41, 0xd1, 0x6a, 0x93, 0x06,
+	0xf5, 0xfd, 0x79, 0x26, 0x73, 0xe4, 0x08, 0x2b, 0x5c, 0x5a, 0x4d, 0x20, 0xf0, 0xe2, 0x62, 0x90,
+	0x61, 0x05, 0x6d, 0xbe, 0x17, 0xdc, 0xf2, 0x98, 0x04, 0x77, 0x3a, 0x82, 0x47, 0x6d, 0x73, 0xe0,
+	0x74, 0x04, 0x0f, 0x96, 0x34, 0x74, 0x54, 0x7a, 0x4c, 0xb9, 0xb3, 0x65, 0xc5, 0x92, 0x7f, 0x9e,
+	0xee, 0x4a, 0xf7, 0xf9, 0xb4, 0xef, 0x3e, 0xa5, 0xe3, 0xfa, 0xef, 0x48, 0x3c, 0xe3, 0x7e, 0x42,
+	0x53, 0x28, 0xc6, 0x36, 0x76, 0x9d, 0x20, 0xce, 0xbd, 0xe9, 0x1f, 0xfe, 0xf3, 0x5d, 0xe6, 0xd9,
+	0x6d, 0xeb, 0x1b, 0x14, 0x35, 0x63, 0x5b, 0xf2, 0xff, 0x59, 0xb6, 0x24, 0x80, 0x49, 0xb3, 0x2f,
+	0x2e, 0xcc, 0xf5, 0x97, 0x4a, 0xb7, 0x37, 0x0b, 0x50, 0xea, 0x32, 0x7a, 0xd6, 0x6a, 0x50, 0xe6,
+	0x89, 0x1d, 0x1a, 0x09, 0xfd, 0xd4, 0x55, 0x9f, 0x80, 0x43, 0x1e, 0xf3, 0x1f, 0x39, 0x40, 0xbd,
+	0xb6, 0xc3, 0x2d, 0xde, 0xa5, 0x8e, 0x7d, 0x15, 0xaf, 0xc6, 0x2d, 0x1e, 0xcb, 0x61, 0xec, 0xd3,
+	0xf9, 0xbc, 0x6a, 0x4d, 0xe2, 0x7a, 0xf1, 0xfc, 0x61, 0x99, 0x0f, 0x62, 0x49, 0x43, 0x6b, 0x30,
+	0xd3, 0x15, 0xc8, 0x1b, 0xc4, 0x6d, 0x50, 0xcf, 0xbf, 0x79, 0xe2, 0x8c, 0x46, 0xaa, 0xff, 0xa5,
+	0x64, 0x66, 0xae, 0x26, 0xf0, 0xe0, 0x44, 0x49, 0xb4, 0x09, 0xa5, 0x6d, 0x7f, 0x9b, 0x94, 0x1b,
+	0x5b, 0x1c, 0xe8, 0x64, 0xa4, 0x2f, 0x08, 0xfe, 0xc5, 0x21, 0x2c, 0xba, 0x02, 0x85, 0x26, 0x6d,
+	0xb5, 0x67, 0x87, 0x04, 0xfc, 0xff, 0x66, 0xbd, 0x0b, 0xd5, 0x11, 0xee, 0xf2, 0xf9, 0x5f, 0x58,
+	0xe0, 0x98, 0xdf, 0x02, 0xb9, 0x2b, 0x59, 0xb6, 0xf7, 0xe0, 0x40, 0xf2, 0x30, 0x0c, 0xef, 0x50,
+	0x37, 0xd8, 0x4e, 0x0d, 0xec, 0x9a, 0x1c, 0xc6, 0x3e, 0xdd, 0xfc, 0xa5, 0x01, 0x53, 0x62, 0x06,
+	0xeb, 0xdd, 0x4d, 0x56, 0x73, 0x2d, 0x87, 0x27, 0x22, 0xb7, 0x77, 0x36, 0x67, 0x61, 0x92, 0xd1,
+	0xf6, 0x0e, 0x75, 0x97, 0xed, 0x0e, 0xf3, 0x5c, 0x62, 0x75, 0x3c, 0x35, 0xad, 0x59, 0xc5, 0x3d,
+	0xb9, 0x1e, 0xa3, 0xe3, 0x1e, 0x09, 0xf3, 0x17, 0x05, 0x18, 0x3e, 0xef, 0x52, 0xab, 0xd1, 0xf4,
+	0xd0, 0xd7, 0x61, 0xa4, 0xad, 0xf2, 0x35, 0x31, 0x3f, 0x7e, 0x12, 0x32, 0x49, 0xae, 0xe8, 0x49,
+	0x72, 0xc5, 0xd9, 0x6e, 0xf0, 0x01, 0x56, 0xe1, 0xdc, 0x95, 0x9d, 0x13, 0x95, 0x17, 0x36, 0x5f,
+	0xa3, 0x35, 0x8f, 0xe7, 0x7a, 0x61, 0x98, 0x0a, 0xc7, 0x70, 0x80, 0xca, 0x4d, 0x98, 0xb4, 0x2c,
+	0xc2, 0x66, 0x87, 0xa3, 0x26, 0xbc, 0xc4, 0x07, 0xb1, 0xa4, 0xf1, 0xab, 0x75, 0x83, 0xb8, 0xb4,
+	0x69, 0x77, 0x19, 0x9d, 0x1d, 0x89, 0xa6, 0x00, 0x2f, 0xfa, 0x04, 0x1c, 0xf2, 0xa0, 0xeb, 0x30,
+	0x5c, 0xb3, 0xdb, 0x6d, 0xcb, 0xf3, 0x5d, 0xf9, 0x42, 0x3a, 0x03, 0xba, 0x60, 0x79, 0xcb, 0x42,
+	0x2e, 0x3c, 0x07, 0xf9, 0x3f, 0xc3, 0x3e, 0x20, 0x5a, 0x0f, 0x9c, 0x52, 0x41, 0x40, 0x3f, 0x92,
+	0x0e, 0x5a, 0xf8, 0x8a, 0x7e, 0xfe, 0x87, 0x83, 0x8a, 0xdb, 0xca, 0x66, 0x87, 0xb2, 0x80, 0x0a,
+	0x83, 0x0a, 0x41, 0xc5, 0xbf, 0x0c, 0x2b, 0x28, 0xf4, 0x72, 0x10, 0xe8, 0x8b, 0xe2, 0xec, 0x4e,
+	0xa5, 0x03, 0x55, 0x87, 0xaf, 0xb2, 0x8c, 0xf1, 0x68, 0x76, 0xe0, 0xe7, 0x01, 0xe6, 0xfb, 0x06,
+	0x94, 0x15, 0xe7, 0xaa, 0xc5, 0x3c, 0xf4, 0x4a, 0x8f, 0xa9, 0x54, 0xd2, 0x99, 0x0a, 0x97, 0x16,
+	0x86, 0x12, 0xe4, 0x11, 0xfe, 0x88, 0x66, 0x26, 0x18, 0x86, 0x2c, 0x8f, 0xb6, 0xfd, 0xb2, 0xe3,
+	0xb1, 0x4c, 0x2b, 0xd1, 0x1c, 0x36, 0xc7, 0xc0, 0x12, 0xca, 0xfc, 0xac, 0x00, 0x93, 0x8a, 0x23,
+	0x43, 0xe6, 0x1c, 0x35, 0xc6, 0x62, 0x36, 0x63, 0xcc, 0xdd, 0x39, 0x63, 0xcc, 0xdf, 0x09, 0x63,
+	0x2c, 0xdc, 0x3e, 0x63, 0x7c, 0x03, 0x26, 0x77, 0xa8, 0x6b, 0x6d, 0x59, 0x35, 0x51, 0x82, 0xad,
+	0x74, 0xb6, 0x6c, 0xe5, 0xdc, 0x9f, 0x48, 0x07, 0x7f, 0x2d, 0x26, 0x5d, 0x9d, 0xe1, 0x0e, 0x2d,
+	0x3e, 0x8a, 0x7b, 0xb4, 0xa0, 0xb7, 0x0d, 0x98, 0xd6, 0x07, 0x2f, 0x5a, 0xcc, 0xb3, 0xdd, 0xdd,
+	0xd9, 0x61, 0xb1, 0xb8, 0x41, 0xb5, 0xdf, 0xa7, 0xd6, 0x39, 0x7d, 0xad, 0x17, 0x1a, 0x27, 0xe9,
+	0x33, 0xff, 0x99, 0x87, 0xb1, 0xc8, 0xdd, 0x42, 0x37, 0x00, 0x24, 0x23, 0xad, 0xaf, 0x74, 0x54,
+	0x8e, 0xb3, 0x3c, 0xc0, 0x25, 0x55, 0xb3, 0xe3, 0x28, 0xb2, 0x94, 0x0e, 0x7c, 0x6e, 0x48, 0xc0,
+	0x9a, 0x2a, 0xf4, 0x26, 0x94, 0x89, 0xaa, 0xfe, 0xce, 0xdb, 0xae, 0x32, 0xcb, 0xb3, 0x83, 0x68,
+	0x5e, 0x0a, 0x61, 0xe2, 0x55, 0x7c, 0x48, 0xc1, 0xba, 0xb6, 0x39, 0x17, 0x26, 0x62, 0xf3, 0x4d,
+	0xa8, 0xc4, 0x57, 0xf4, 0x4a, 0x3c, 0xb5, 0xeb, 0xf2, 0x71, 0x45, 0x49, 0xab, 0x97, 0xff, 0x0c,
+	0x26, 0xe3, 0x33, 0xbd, 0x6d, 0x4a, 0x23, 0x75, 0xb4, 0xde, 0x33, 0xf8, 0x5d, 0x0e, 0x4a, 0xc1,
+	0x25, 0xce, 0x12, 0xea, 0xe7, 0x20, 0x67, 0xd5, 0x55, 0xa0, 0x07, 0xc5, 0x95, 0x5b, 0x39, 0x8b,
+	0x73, 0x56, 0x1d, 0x3d, 0x08, 0xc5, 0x4d, 0x97, 0x74, 0x6a, 0x4d, 0x15, 0xda, 0x83, 0xfb, 0x56,
+	0x15, 0xa3, 0x58, 0x51, 0x79, 0xaa, 0xee, 0x91, 0x86, 0x48, 0xcf, 0xb4, 0x54, 0x7d, 0x83, 0x34,
+	0x30, 0x1f, 0x47, 0x17, 0x60, 0x4a, 0xd6, 0xa6, 0xcb, 0x4d, 0x5a, 0xdb, 0x96, 0x53, 0x14, 0xf7,
+	0xb1, 0x54, 0xbd, 0x57, 0x31, 0x4f, 0x5d, 0x8c, 0x33, 0xe0, 0x5e, 0x19, 0xbd, 0xba, 0x2f, 0xee,
+	0x5f, 0xdd, 0xf3, 0xa9, 0x93, 0xae, 0xd7, 0xb4, 0x5d, 0x15, 0xec, 0x83, 0xa9, 0x2f, 0x89, 0x51,
+	0xac, 0xa8, 0xe6, 0x34, 0x4c, 0x5d, 0xb0, 0xbc, 0x8b, 0xdd, 0xcd, 0xb5, 0x6e, 0xab, 0x85, 0xe9,
+	0xeb, 0x5d, 0x9e, 0x2d, 0xcb, 0xc1, 0x55, 0x12, 0x19, 0xfc, 0xd5, 0x10, 0x8c, 0x5d, 0xb0, 0x3c,
+	0xb1, 0x81, 0x99, 0xb3, 0xe7, 0x75, 0x38, 0x6c, 0x75, 0x18, 0xad, 0x75, 0x5d, 0xba, 0xbe, 0x6d,
+	0x39, 0x1b, 0xab, 0xeb, 0xc2, 0x7c, 0x76, 0x55, 0xf2, 0x7e, 0x54, 0x09, 0x1e, 0x5e, 0x49, 0x62,
+	0xc2, 0xc9, 0xb2, 0xe8, 0x24, 0x80, 0x4b, 0x49, 0xbd, 0xaa, 0x1f, 0x51, 0x70, 0x1b, 0x71, 0x40,
+	0xc1, 0x1a, 0x17, 0x5a, 0x84, 0xf2, 0x0d, 0xd7, 0xf2, 0xa8, 0x12, 0x92, 0x47, 0x16, 0xdc, 0xa3,
+	0x17, 0x43, 0x12, 0xd6, 0xf9, 0xd0, 0x0e, 0x94, 0x9d, 0x70, 0x2f, 0x94, 0x33, 0x4d, 0xe9, 0x3e,
+	0xb4, 0x4d, 0x5c, 0x73, 0xed, 0xb6, 0xcd, 0xfd, 0xd4, 0x65, 0x5a, 0x6b, 0x92, 0x8e, 0xc5, 0xda,
+	0xd5, 0x09, 0xae, 0x57, 0x63, 0xc1, 0xba, 0x22, 0xd4, 0x80, 0xa2, 0x4b, 0x3b, 0x75, 0xea, 0xaa,
+	0xb4, 0x22, 0xa5, 0xca, 0xe7, 0xf9, 0x10, 0x16, 0x82, 0x09, 0x2a, 0x81, 0xdb, 0x81, 0xa4, 0x62,
+	0x05, 0x8f, 0x3a, 0x7a, 0x9d, 0x31, 0x2c, 0x74, 0x2d, 0xa5, 0xd4, 0xe5, 0x8b, 0x25, 0x68, 0xea,
+	0x5f, 0x73, 0x5c, 0x57, 0x35, 0xc7, 0x88, 0x50, 0xf5, 0x4c, 0x3a, 0x55, 0xbc, 0xc6, 0x48, 0xd0,
+	0x12, 0xaf, 0x3f, 0xfe, 0x58, 0x80, 0x89, 0x0b, 0xd6, 0xc0, 0xc9, 0xbf, 0x07, 0xf7, 0xc8, 0x90,
+	0xbf, 0x4e, 0x5b, 0xb4, 0xc6, 0xa5, 0xd7, 0x3d, 0x97, 0x78, 0xb4, 0xe1, 0x17, 0xe3, 0x67, 0x94,
+	0xe8, 0x3d, 0xcb, 0xc9, 0x6c, 0xb7, 0xfa, 0x93, 0x70, 0x3f, 0xe8, 0xd4, 0xbe, 0x26, 0xa9, 0xf0,
+	0x28, 0x64, 0x2d, 0x3c, 0x78, 0x62, 0x45, 0x5a, 0x2d, 0xfb, 0xc6, 0x06, 0x69, 0x30, 0xe5, 0x8a,
+	0x82, 0xc4, 0x6a, 0xc9, 0x27, 0xe0, 0x90, 0x07, 0x55, 0x00, 0xac, 0x46, 0xc7, 0x76, 0xa9, 0x90,
+	0x28, 0x8a, 0xee, 0xd2, 0x38, 0xbf, 0x67, 0x2b, 0xc1, 0x28, 0xd6, 0x38, 0xfa, 0x5f, 0xf8, 0xe1,
+	0x2f, 0x70, 0xe1, 0x1f, 0x87, 0x51, 0xab, 0x53, 0x6b, 0x75, 0xeb, 0x74, 0x8d, 0x78, 0x4d, 0x36,
+	0x3b, 0x22, 0xa6, 0x31, 0xb9, 0x77, 0xf3, 0xd8, 0xe8, 0x8a, 0x36, 0x8e, 0x23, 0x5c, 0x5c, 0x8a,
+	0xbe, 0xa1, 0x49, 0x95, 0x42, 0xa9, 0x73, 0x6f, 0xe8, 0x52, 0x3a, 0x97, 0xf9, 0x67, 0x03, 0x8a,
+	0xd2, 0x29, 0xa3, 0xc5, 0x58, 0x13, 0xef, 0x68, 0x4f, 0x13, 0xaf, 0x9c, 0xd4, 0x8b, 0x35, 0xa1,
+	0x68, 0x31, 0xd6, 0xa5, 0x32, 0x15, 0x2d, 0xc9, 0x6b, 0xb7, 0x22, 0x46, 0xb0, 0xa2, 0x20, 0x0b,
+	0x80, 0xf8, 0x5d, 0x38, 0x3f, 0xaf, 0x5c, 0xcc, 0xda, 0xa6, 0x8c, 0xb5, 0x28, 0x03, 0x02, 0xc3,
+	0x1a, 0xb8, 0xf9, 0x73, 0x03, 0xee, 0xe5, 0x97, 0x44, 0xe4, 0x8a, 0x67, 0xa9, 0xc3, 0xef, 0x7d,
+	0xa7, 0xb6, 0xab, 0x7c, 0xb9, 0xf0, 0xa5, 0x8e, 0xcd, 0x2c, 0x91, 0xae, 0x19, 0x71, 0x5f, 0xea,
+	0x53, 0xb0, 0xc6, 0x95, 0xa2, 0x4a, 0x5e, 0x80, 0x92, 0x48, 0x49, 0xf9, 0x96, 0x2a, 0xbb, 0x0e,
+	0xcc, 0x6c, 0xd9, 0x27, 0xe0, 0x90, 0xc7, 0xfc, 0x8b, 0x01, 0x13, 0x03, 0x75, 0xcb, 0x9e, 0x85,
+	0x71, 0x91, 0x0c, 0xb0, 0xf3, 0x56, 0x4b, 0x9c, 0xa0, 0x9a, 0xd5, 0x11, 0xc5, 0x3d, 0x7e, 0x2d,
+	0x42, 0xc5, 0x31, 0x6e, 0xbf, 0xdb, 0x96, 0x3f, 0xa8, 0xdb, 0x56, 0x18, 0xa0, 0xdb, 0xf6, 0x89,
+	0x01, 0x47, 0x92, 0x5d, 0x17, 0x7a, 0x35, 0xd6, 0x75, 0x5b, 0x4c, 0xef, 0x08, 0x53, 0xb4, 0xda,
+	0x78, 0xf8, 0x50, 0xd5, 0x85, 0x4c, 0x3b, 0x9f, 0x4b, 0x0f, 0x9f, 0x68, 0x26, 0xfd, 0x2a, 0x0e,
+	0xf3, 0x37, 0x06, 0xc8, 0xf3, 0xc8, 0xe2, 0x68, 0x4f, 0x02, 0x34, 0x54, 0x42, 0x81, 0x57, 0xd5,
+	0x79, 0x05, 0x36, 0x77, 0x21, 0xa0, 0x60, 0x8d, 0xcb, 0x4f, 0xb5, 0xf2, 0x7d, 0x52, 0xad, 0x07,
+	0xa1, 0x58, 0x97, 0x5d, 0xc1, 0x42, 0xd4, 0x8b, 0xaa, 0x96, 0xa0, 0xa2, 0x9a, 0xef, 0x16, 0x60,
+	0x4a, 0xcc, 0x77, 0xd0, 0x20, 0x31, 0xc8, 0xdc, 0x1d, 0x38, 0x22, 0xce, 0xa5, 0x37, 0xae, 0xc8,
+	0xe5, 0x9c, 0x56, 0xf2, 0x47, 0x56, 0x12, 0xb9, 0x6e, 0xf5, 0xa5, 0xe0, 0x3e, 0xb8, 0xff, 0x29,
+	0xc1, 0xe2, 0x51, 0x18, 0x71, 0x5a, 0xc4, 0xdb, 0xb2, 0xdd, 0xb6, 0x4a, 0x57, 0x83, 0xfe, 0xc4,
+	0x9a, 0x1a, 0xc7, 0x01, 0x47, 0xff, 0xd0, 0x32, 0x32, 0x78, 0x68, 0x31, 0x3b, 0x70, 0x44, 0x4b,
+	0x9a, 0xee, 0x7c, 0x43, 0xfa, 0x6d, 0x03, 0x8e, 0xee, 0x9b, 0xa5, 0xa1, 0x7a, 0xcc, 0x35, 0x3c,
+	0x93, 0x39, 0xf5, 0x4b, 0xd3, 0x8c, 0x7f, 0xd7, 0x80, 0x99, 0xc1, 0xfb, 0xf0, 0xc7, 0xa1, 0xe0,
+	0x84, 0xbe, 0x36, 0x88, 0x00, 0xc2, 0xc3, 0x0a, 0x4a, 0x74, 0x63, 0xf2, 0x29, 0x36, 0xe6, 0x2d,
+	0x03, 0xee, 0xdb, 0x27, 0xa5, 0x44, 0x9b, 0xb1, 0x6d, 0x39, 0x93, 0x31, 0x4b, 0x4d, 0xb3, 0x29,
+	0x3f, 0xcd, 0xc1, 0xf0, 0x9a, 0x6b, 0xbf, 0x46, 0x6b, 0x77, 0xa3, 0x2d, 0xfb, 0x02, 0x14, 0x98,
+	0x43, 0x6b, 0xaa, 0x10, 0x3e, 0x91, 0xb2, 0xa8, 0x90, 0xd3, 0x5b, 0x77, 0x68, 0x4d, 0xe6, 0xbf,
+	0xfc, 0x2f, 0x2c, 0x80, 0xb4, 0x5e, 0x64, 0x3e, 0x4b, 0x6d, 0xed, 0x43, 0x1e, 0xdc, 0x8b, 0x54,
+	0x9c, 0x5f, 0xda, 0x5e, 0xa4, 0x9a, 0x5f, 0x9f, 0x5e, 0xe4, 0x0f, 0xc2, 0x15, 0xf0, 0x4d, 0x43,
+	0xdf, 0x84, 0x29, 0xc7, 0xb7, 0xb3, 0x35, 0xbb, 0x65, 0xd5, 0xac, 0xac, 0xe1, 0x78, 0x2d, 0x22,
+	0xbe, 0x1b, 0x56, 0xf5, 0x6b, 0x71, 0x5c, 0xdc, 0xab, 0xca, 0xb4, 0x61, 0x2c, 0xb2, 0xf5, 0xe8,
+	0x94, 0xff, 0x26, 0x21, 0x9a, 0x6e, 0xca, 0x37, 0x09, 0xb7, 0x6e, 0x1e, 0x1b, 0x55, 0xec, 0xfa,
+	0x1b, 0x85, 0x2c, 0x5f, 0xfe, 0x7f, 0x96, 0x83, 0x52, 0x30, 0xb3, 0xbb, 0x60, 0xe0, 0x5f, 0x89,
+	0x18, 0xf8, 0xa9, 0x8c, 0x7b, 0x9a, 0x68, 0xe2, 0xaf, 0xc6, 0x4c, 0x3c, 0xeb, 0x41, 0x1d, 0x60,
+	0xe4, 0x9f, 0x1b, 0xe2, 0x4c, 0x24, 0xaf, 0x68, 0x6c, 0x1e, 0xdc, 0xab, 0x26, 0x30, 0xbc, 0x25,
+	0xdb, 0x75, 0x6a, 0xa1, 0x4f, 0x64, 0xea, 0xf1, 0x05, 0x6d, 0xf1, 0xf0, 0xe0, 0x7c, 0x8a, 0x8f,
+	0x8b, 0xbe, 0x7a, 0x7b, 0x56, 0x0d, 0x09, 0x2b, 0xfe, 0x40, 0x5f, 0xf1, 0x5d, 0xb8, 0xd8, 0x1b,
+	0xd1, 0x8b, 0xbd, 0x90, 0x71, 0x25, 0x7d, 0xae, 0xf6, 0xf7, 0x73, 0x30, 0xdd, 0x1b, 0x33, 0x18,
+	0x62, 0x30, 0xde, 0xd0, 0x5b, 0x57, 0xfe, 0xfd, 0x3e, 0x95, 0xfa, 0xeb, 0x40, 0x28, 0x1b, 0x96,
+	0x14, 0x91, 0x61, 0x86, 0x63, 0x2a, 0xd0, 0x9b, 0x30, 0x49, 0xa2, 0x2f, 0x2c, 0xfc, 0xd5, 0x66,
+	0xad, 0xf0, 0x94, 0xe2, 0x20, 0x67, 0x8b, 0x11, 0x18, 0xee, 0x51, 0x64, 0xbe, 0x63, 0xc0, 0x44,
+	0xcc, 0x2d, 0xf1, 0x90, 0xce, 0xbc, 0x84, 0x90, 0xae, 0x9a, 0xa9, 0x82, 0x86, 0xd6, 0x60, 0x86,
+	0x74, 0x3d, 0x3b, 0x90, 0x3d, 0xd7, 0x21, 0x9b, 0x2d, 0x5a, 0x57, 0x49, 0x4d, 0xf0, 0x09, 0x7b,
+	0x29, 0x81, 0x07, 0x27, 0x4a, 0x9a, 0x5f, 0xd3, 0x2c, 0x4b, 0x38, 0xdc, 0x54, 0xf3, 0x78, 0x38,
+	0x7a, 0x9d, 0x4a, 0xfd, 0xaf, 0x85, 0xf9, 0x79, 0x4e, 0x5b, 0xab, 0xf2, 0xa1, 0x97, 0x00, 0xb5,
+	0x08, 0xf3, 0x2e, 0x92, 0x4e, 0x9d, 0xcf, 0x8c, 0x6e, 0xb9, 0x94, 0xf9, 0xed, 0xbe, 0x39, 0x85,
+	0x84, 0x56, 0x7b, 0x38, 0x70, 0x82, 0x14, 0x5a, 0x8c, 0xfa, 0xe3, 0x63, 0x71, 0x7f, 0x3c, 0x1e,
+	0x6e, 0xf4, 0x60, 0x1e, 0x19, 0xbd, 0xae, 0xdd, 0xb5, 0x7c, 0x96, 0x4f, 0x13, 0xb1, 0x65, 0x57,
+	0xfc, 0x17, 0x7f, 0xf2, 0xfb, 0x40, 0x70, 0x01, 0xfd, 0xe1, 0xf0, 0x02, 0xce, 0x3d, 0x0d, 0x63,
+	0x11, 0xe6, 0x4c, 0x2f, 0xf4, 0xfe, 0x6a, 0xc0, 0xd1, 0x7d, 0xdb, 0x9a, 0x3c, 0x07, 0x91, 0x0b,
+	0x50, 0xbe, 0xe3, 0xc9, 0xd4, 0x37, 0x2d, 0xda, 0x8b, 0x96, 0xce, 0x4a, 0x0e, 0x63, 0x05, 0xa9,
+	0xc0, 0x5b, 0x64, 0x53, 0x79, 0xda, 0xf4, 0xe0, 0xd1, 0x9e, 0x76, 0x00, 0xbe, 0x4a, 0x24, 0x78,
+	0x8b, 0x6c, 0x9a, 0xef, 0xe5, 0x60, 0x92, 0x5f, 0xe3, 0x48, 0x65, 0xb8, 0x06, 0xf9, 0x86, 0xe5,
+	0xa9, 0xb5, 0x2c, 0xa6, 0x56, 0xa7, 0x63, 0x54, 0x87, 0x79, 0xa5, 0xca, 0x7d, 0x06, 0x87, 0x42,
+	0x2f, 0xf9, 0xf9, 0x75, 0xa6, 0x25, 0xf4, 0xd4, 0xac, 0xd5, 0x52, 0x4f, 0x52, 0xfe, 0x92, 0xff,
+	0x52, 0x25, 0x9f, 0x05, 0xb9, 0xe7, 0xbd, 0x84, 0x44, 0xd6, 0x9f, 0xb7, 0x98, 0x3f, 0xc9, 0x81,
+	0xbc, 0xa4, 0x77, 0x21, 0x69, 0xb8, 0x1c, 0x49, 0x1a, 0x52, 0xc6, 0x07, 0x31, 0xb9, 0xc4, 0x84,
+	0x21, 0x1e, 0x3a, 0x4f, 0x64, 0x01, 0xdc, 0x3f, 0x59, 0xf8, 0x83, 0x01, 0x25, 0xc1, 0x77, 0x17,
+	0xc2, 0xe6, 0x5a, 0x34, 0x6c, 0x3e, 0x92, 0x61, 0x15, 0x7d, 0x42, 0xe6, 0x0f, 0xf3, 0x6a, 0xf6,
+	0x81, 0x6b, 0x6e, 0x12, 0xb7, 0xae, 0x3c, 0x65, 0xe8, 0x9a, 0xf9, 0x20, 0x96, 0x34, 0xd4, 0x82,
+	0x31, 0xa6, 0x19, 0x0a, 0x53, 0xeb, 0x4c, 0x19, 0x4c, 0x75, 0x1b, 0x63, 0xd5, 0x29, 0xf1, 0x72,
+	0x4f, 0x1f, 0xc2, 0x51, 0x70, 0xf4, 0x3d, 0x03, 0xa6, 0x9d, 0xde, 0x98, 0xae, 0x0c, 0xe3, 0xa9,
+	0x8c, 0x7e, 0x32, 0x04, 0xa8, 0xde, 0xb3, 0x77, 0xf3, 0x58, 0x52, 0xb6, 0x80, 0x93, 0xd4, 0xa1,
+	0x26, 0x8c, 0xea, 0x1f, 0x9a, 0x95, 0x19, 0x9d, 0xcc, 0xfe, 0x45, 0x5b, 0x76, 0x9e, 0xf5, 0x11,
+	0x1c, 0x41, 0x36, 0x7f, 0x5c, 0x84, 0xb2, 0x66, 0x77, 0x7d, 0x42, 0x59, 0x79, 0xa0, 0x50, 0x76,
+	0x22, 0x1a, 0xca, 0xee, 0x8b, 0x87, 0x32, 0x10, 0x8a, 0x23, 0x61, 0xcc, 0x85, 0xf1, 0x5a, 0xd7,
+	0x75, 0x69, 0xc7, 0x3b, 0x7f, 0x5b, 0xd2, 0x5b, 0xc4, 0x53, 0xa7, 0xe5, 0x08, 0x22, 0x8e, 0x69,
+	0xe0, 0xb9, 0x74, 0x53, 0xbd, 0x1c, 0xc8, 0x67, 0x79, 0x39, 0xd0, 0x3f, 0x97, 0xf6, 0x5f, 0x0b,
+	0xf8, 0xb8, 0x68, 0x0d, 0x8a, 0xf2, 0x03, 0xab, 0xfa, 0x04, 0xf5, 0x68, 0xda, 0xd6, 0x28, 0x97,
+	0x91, 0x81, 0x43, 0xfe, 0x8d, 0x15, 0x8e, 0x1e, 0xef, 0x4b, 0x07, 0xc4, 0xfb, 0x4b, 0x80, 0xec,
+	0x4d, 0x46, 0xdd, 0x1d, 0x5a, 0xbf, 0x20, 0x7f, 0xe0, 0xc0, 0x4d, 0xaa, 0x78, 0xdc, 0x78, 0x28,
+	0x1f, 0x1e, 0xe9, 0x0b, 0x3d, 0x1c, 0x38, 0x41, 0x0a, 0x75, 0x61, 0x52, 0xed, 0x5e, 0x60, 0xcb,
+	0xea, 0x03, 0x5e, 0xd6, 0x4a, 0x2b, 0x7c, 0xe9, 0xb1, 0x1c, 0x03, 0xc4, 0x3d, 0x2a, 0xb8, 0x13,
+	0xe0, 0xf6, 0x15, 0xea, 0x84, 0xc1, 0x75, 0x0a, 0x27, 0xb0, 0xaa, 0xa3, 0xe1, 0x28, 0xb8, 0xb9,
+	0x08, 0x53, 0xf2, 0x4a, 0xe8, 0x41, 0xf9, 0xe0, 0x97, 0xf7, 0xbf, 0x37, 0x20, 0xea, 0x5c, 0xa2,
+	0x2f, 0x8a, 0x8c, 0x14, 0x2f, 0x8a, 0x6e, 0xc0, 0x78, 0xd7, 0x61, 0x9e, 0x4b, 0x49, 0x5b, 0xcc,
+	0xc0, 0x77, 0xbd, 0x4f, 0x66, 0x09, 0x20, 0x7a, 0x58, 0x0d, 0xca, 0x87, 0xab, 0x11, 0x58, 0x1c,
+	0x53, 0x63, 0xfe, 0x2b, 0x07, 0x11, 0x2f, 0x81, 0xde, 0x31, 0x60, 0x8a, 0xc4, 0x7e, 0x86, 0xe0,
+	0x17, 0x32, 0xcf, 0x65, 0xfb, 0x6d, 0x48, 0xcf, 0xaf, 0x18, 0xc2, 0x96, 0x45, 0x9c, 0x85, 0xe1,
+	0x5e, 0xa5, 0xc2, 0x27, 0x93, 0xde, 0xdf, 0x99, 0x64, 0xf3, 0xc9, 0x09, 0x3f, 0x54, 0x91, 0x3e,
+	0x39, 0x81, 0x80, 0x93, 0xd4, 0xa1, 0x97, 0xa1, 0x40, 0xdc, 0x86, 0xff, 0xdd, 0x2c, 0xbb, 0x5a,
+	0xff, 0xe7, 0x43, 0xa1, 0xed, 0x2c, 0xb9, 0x0d, 0x86, 0x05, 0xa8, 0xf9, 0xb7, 0x3c, 0xf4, 0xbc,
+	0x78, 0x52, 0xaf, 0x45, 0x0a, 0x89, 0xaf, 0x45, 0xee, 0x87, 0x21, 0x52, 0xf3, 0x82, 0x17, 0x17,
+	0xe1, 0xf3, 0x4a, 0x3e, 0x88, 0x25, 0x0d, 0xbd, 0x08, 0x25, 0xe6, 0x11, 0xd7, 0xdb, 0xb0, 0xda,
+	0x54, 0x3d, 0x23, 0xf8, 0x9f, 0x74, 0xf9, 0x01, 0x97, 0x90, 0x1f, 0xd4, 0xd7, 0x7d, 0x00, 0x1c,
+	0x62, 0xa1, 0xd3, 0x51, 0xcf, 0x6e, 0xc6, 0x3d, 0xfb, 0x94, 0xbe, 0x96, 0x41, 0xeb, 0x94, 0x36,
+	0x94, 0xb5, 0x73, 0x50, 0x31, 0xf0, 0x4c, 0xe6, 0x7d, 0xd7, 0xfc, 0xb3, 0xfc, 0x0d, 0x52, 0x48,
+	0xd1, 0xf1, 0xd1, 0x75, 0x80, 0x2d, 0xab, 0x63, 0xb1, 0xa6, 0xd8, 0xad, 0x62, 0xe6, 0xdd, 0x12,
+	0xdf, 0x1c, 0xce, 0x07, 0x08, 0x58, 0x43, 0x33, 0x27, 0x60, 0x2c, 0xf2, 0x82, 0x49, 0x74, 0xc5,
+	0x02, 0x0f, 0xf0, 0x65, 0xed, 0x8a, 0x05, 0x13, 0xbc, 0x9d, 0x5d, 0xb1, 0x10, 0x74, 0xff, 0x44,
+	0xf7, 0x03, 0x03, 0xc6, 0x02, 0xde, 0x2f, 0x6d, 0x8f, 0x28, 0x98, 0x61, 0x9f, 0x84, 0xf7, 0xd7,
+	0xfa, 0x2a, 0xa2, 0x49, 0x6f, 0x6e, 0x9f, 0xa4, 0x97, 0xf5, 0x26, 0xbd, 0x19, 0x12, 0x93, 0x78,
+	0x41, 0xa9, 0xfd, 0x6a, 0x65, 0x9f, 0xdc, 0xd7, 0x7c, 0x3f, 0x07, 0x13, 0xb1, 0xd3, 0xe9, 0x93,
+	0x0e, 0x16, 0x07, 0x4a, 0x07, 0xb5, 0xab, 0x9f, 0x1f, 0x28, 0x65, 0x29, 0x0c, 0x94, 0xb2, 0x58,
+	0x50, 0xe6, 0x93, 0xf1, 0xf3, 0xc9, 0xa1, 0x2f, 0xd6, 0x2e, 0xe5, 0x2e, 0x64, 0x35, 0x84, 0xc3,
+	0x3a, 0x76, 0xf5, 0xd2, 0x87, 0x9f, 0xce, 0x1f, 0xfa, 0xe8, 0xd3, 0xf9, 0x43, 0x1f, 0x7f, 0x3a,
+	0x7f, 0xe8, 0xdb, 0x7b, 0xf3, 0xc6, 0x87, 0x7b, 0xf3, 0xc6, 0x47, 0x7b, 0xf3, 0xc6, 0xc7, 0x7b,
+	0xf3, 0xc6, 0x27, 0x7b, 0xf3, 0xc6, 0x8f, 0xfe, 0x3e, 0x7f, 0xe8, 0xfa, 0x03, 0x69, 0x7e, 0x7f,
+	0xfb, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x19, 0x4d, 0x94, 0xce, 0xa6, 0x3b, 0x00, 0x00,
 }
 
 func (m *AnalysisRunArgument) Marshal() (dAtA []byte, err error) {
@@ -4199,6 +4202,11 @@ func (m *PromotionStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	i -= len(m.LastHandledRefresh)
+	copy(dAtA[i:], m.LastHandledRefresh)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.LastHandledRefresh)))
+	i--
+	dAtA[i] = 0x22
 	if len(m.Metadata) > 0 {
 		keysForMetadata := make([]string, 0, len(m.Metadata))
 		for k := range m.Metadata {
@@ -4528,6 +4536,11 @@ func (m *StageStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	i -= len(m.LastHandledRefresh)
+	copy(dAtA[i:], m.LastHandledRefresh)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.LastHandledRefresh)))
+	i--
+	dAtA[i] = 0x5a
 	if m.LastPromotion != nil {
 		{
 			size, err := m.LastPromotion.MarshalToSizedBuffer(dAtA[:i])
@@ -4759,6 +4772,11 @@ func (m *VerificationInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	i -= len(m.Actor)
+	copy(dAtA[i:], m.Actor)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Actor)))
+	i--
+	dAtA[i] = 0x3a
 	if m.FinishTime != nil {
 		{
 			size, err := m.FinishTime.MarshalToSizedBuffer(dAtA[:i])
@@ -5000,6 +5018,11 @@ func (m *WarehouseStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	i -= len(m.LastHandledRefresh)
+	copy(dAtA[i:], m.LastHandledRefresh)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.LastHandledRefresh)))
+	i--
+	dAtA[i] = 0x32
 	if m.LastFreight != nil {
 		{
 			size, err := m.LastFreight.MarshalToSizedBuffer(dAtA[:i])
@@ -5869,6 +5892,8 @@ func (m *PromotionStatus) Size() (n int) {
 			n += mapEntrySize + 1 + sovGenerated(uint64(mapEntrySize))
 		}
 	}
+	l = len(m.LastHandledRefresh)
+	n += 1 + l + sovGenerated(uint64(l))
 	return n
 }
 
@@ -6000,6 +6025,8 @@ func (m *StageStatus) Size() (n int) {
 		l = m.LastPromotion.Size()
 		n += 1 + l + sovGenerated(uint64(l))
 	}
+	l = len(m.LastHandledRefresh)
+	n += 1 + l + sovGenerated(uint64(l))
 	return n
 }
 
@@ -6080,6 +6107,8 @@ func (m *VerificationInfo) Size() (n int) {
 		l = m.FinishTime.Size()
 		n += 1 + l + sovGenerated(uint64(l))
 	}
+	l = len(m.Actor)
+	n += 1 + l + sovGenerated(uint64(l))
 	return n
 }
 
@@ -6156,6 +6185,8 @@ func (m *WarehouseStatus) Size() (n int) {
 		l = m.LastFreight.Size()
 		n += 1 + l + sovGenerated(uint64(l))
 	}
+	l = len(m.LastHandledRefresh)
+	n += 1 + l + sovGenerated(uint64(l))
 	return n
 }
 
@@ -6866,6 +6897,7 @@ func (this *PromotionStatus) String() string {
 		`Phase:` + fmt.Sprintf("%v", this.Phase) + `,`,
 		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
 		`Metadata:` + mapStringForMetadata + `,`,
+		`LastHandledRefresh:` + fmt.Sprintf("%v", this.LastHandledRefresh) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -6952,6 +6984,7 @@ func (this *StageStatus) String() string {
 		`Health:` + strings.Replace(this.Health.String(), "Health", "Health", 1) + `,`,
 		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
 		`LastPromotion:` + strings.Replace(this.LastPromotion.String(), "PromotionInfo", "PromotionInfo", 1) + `,`,
+		`LastHandledRefresh:` + fmt.Sprintf("%v", this.LastHandledRefresh) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -7015,6 +7048,7 @@ func (this *VerificationInfo) String() string {
 		`ID:` + fmt.Sprintf("%v", this.ID) + `,`,
 		`StartTime:` + strings.Replace(fmt.Sprintf("%v", this.StartTime), "Time", "v1.Time", 1) + `,`,
 		`FinishTime:` + strings.Replace(fmt.Sprintf("%v", this.FinishTime), "Time", "v1.Time", 1) + `,`,
+		`Actor:` + fmt.Sprintf("%v", this.Actor) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -7080,6 +7114,7 @@ func (this *WarehouseStatus) String() string {
 		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
 		`ObservedGeneration:` + fmt.Sprintf("%v", this.ObservedGeneration) + `,`,
 		`LastFreight:` + strings.Replace(this.LastFreight.String(), "FreightReference", "FreightReference", 1) + `,`,
+		`LastHandledRefresh:` + fmt.Sprintf("%v", this.LastHandledRefresh) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -14200,6 +14235,38 @@ func (m *PromotionStatus) Unmarshal(dAtA []byte) error {
 			}
 			m.Metadata[mapkey] = mapvalue
 			iNdEx = postIndex
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field LastHandledRefresh", 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.LastHandledRefresh = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
@@ -15250,6 +15317,38 @@ func (m *StageStatus) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 11:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field LastHandledRefresh", 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.LastHandledRefresh = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
@@ -15856,6 +15955,38 @@ func (m *VerificationInfo) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 7:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Actor", 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.Actor = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
@@ -16428,6 +16559,38 @@ func (m *WarehouseStatus) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 6:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field LastHandledRefresh", 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.LastHandledRefresh = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto
index 1dcc458d..cf8e13a1 100644
--- a/api/v1alpha1/generated.proto
+++ b/api/v1alpha1/generated.proto
@@ -891,6 +891,12 @@ message PromotionSpec {
 // PromotionStatus describes the current state of the transition represented by
 // a Promotion.
 message PromotionStatus {
+  // LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+  // annotation that was handled by the controller. This field can be used to
+  // determine whether the request to refresh the resource has been handled.
+  // +optional
+  optional string lastHandledRefresh = 4;
+
   // Phase describes where the Promotion currently is in its lifecycle.
   optional string phase = 1;
 
@@ -981,6 +987,12 @@ message StageSpec {
 // StageStatus describes a Stages's current and recent Freight, health, and
 // more.
 message StageStatus {
+  // LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+  // annotation that was handled by the controller. This field can be used to
+  // determine whether the request to refresh the resource has been handled.
+  // +optional
+  optional string lastHandledRefresh = 11;
+
   // Phase describes where the Stage currently is in its lifecycle.
   optional string phase = 1;
 
@@ -1052,6 +1064,10 @@ message VerificationInfo {
   // ID is the identifier of the Verification process.
   optional string id = 4;
 
+  // Actor is the name of the entity that initiated or aborted the
+  // Verification process.
+  optional string actor = 7;
+
   // StartTime is the time at which the Verification process was started.
   optional k8s.io.apimachinery.pkg.apis.meta.v1.Time startTime = 5;
 
@@ -1119,6 +1135,12 @@ message WarehouseSpec {
 
 // WarehouseStatus describes a Warehouse's most recently observed state.
 message WarehouseStatus {
+  // LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+  // annotation that was handled by the controller. This field can be used to
+  // determine whether the request to refresh the resource has been handled.
+  // +optional
+  optional string lastHandledRefresh = 6;
+
   // Message describes any errors that are preventing the Warehouse controller
   // from polling repositories to discover new Freight.
   optional string message = 3;
diff --git a/api/v1alpha1/promotion_types.go b/api/v1alpha1/promotion_types.go
index 438566cb..144848f4 100644
--- a/api/v1alpha1/promotion_types.go
+++ b/api/v1alpha1/promotion_types.go
@@ -86,6 +86,11 @@ type PromotionSpec struct {
 // PromotionStatus describes the current state of the transition represented by
 // a Promotion.
 type PromotionStatus struct {
+	// LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+	// annotation that was handled by the controller. This field can be used to
+	// determine whether the request to refresh the resource has been handled.
+	// +optional
+	LastHandledRefresh string `json:"lastHandledRefresh,omitempty" protobuf:"bytes,4,opt,name=lastHandledRefresh"`
 	// Phase describes where the Promotion currently is in its lifecycle.
 	Phase PromotionPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase"`
 	// Message is a display message about the promotion, including any errors
diff --git a/api/v1alpha1/stage_helpers.go b/api/v1alpha1/stage_helpers.go
index b1a6f764..8a2f4e3d 100644
--- a/api/v1alpha1/stage_helpers.go
+++ b/api/v1alpha1/stage_helpers.go
@@ -2,18 +2,78 @@ package v1alpha1
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"time"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/akuity/kargo/internal/api/user"
 )
 
+// VerificationRequest is a request payload with an optional actor field which
+// can be used to annotate a Stage using the AnnotationKeyReverify or
+// AnnotationKeyAbort annotations.
+//
+// The ID field is used to specify the VerificationInfo to be re-verified or
+// aborted. If the ID is empty, the request is considered invalid. The Actor
+// field is optional and can be used to track the user who initiated the
+// re-verification or abort request. The ControlPlane field is optional and
+// indicates if the request was initiated by a control plane.
+//
+// +protobuf=false
+// +k8s:deepcopy-gen=false
+// +k8s:openapi-gen=false
+type VerificationRequest struct {
+	// ID is the identifier of the VerificationInfo for which the request is
+	// being made.
+	ID string `json:"id,omitempty"`
+	// Actor is the user who initiated the request.
+	Actor string `json:"actor,omitempty"`
+	// ControlPlane is a flag to indicate if the request has been initiated by
+	// a control plane.
+	ControlPlane bool `json:"controlPlane,omitempty"`
+}
+
+// Equals returns true if the VerificationRequest is equal to the other
+// VerificationRequest, false otherwise. Two VerificationRequests are equal if
+// their ID, Actor, and ControlPlane fields are equal.
+func (r *VerificationRequest) Equals(other *VerificationRequest) bool {
+	if r == nil && other == nil {
+		return true
+	}
+	if r == nil || other == nil {
+		return false
+	}
+	return r.ID == other.ID && r.Actor == other.Actor && r.ControlPlane == other.ControlPlane
+}
+
+// HasID returns true if the VerificationRequest has a non-empty ID.
+func (r *VerificationRequest) HasID() bool {
+	return r != nil && r.ID != ""
+}
+
+// ForID returns true if the VerificationRequest has the specified ID.
+func (r *VerificationRequest) ForID(id string) bool {
+	return r != nil && r.ID != "" && r.ID == id
+}
+
+// String returns the JSON string representation of the VerificationRequest,
+// or an empty string if the VerificationRequest is nil or has an empty ID.
+func (r *VerificationRequest) String() string {
+	if r == nil || r.ID == "" {
+		return ""
+	}
+	b, _ := json.Marshal(r)
+	if b == nil {
+		return ""
+	}
+	return string(b)
+}
+
 // GetStage returns a pointer to the Stage resource specified by the
 // namespacedName argument. If no such resource is found, nil is returned
 // instead.
@@ -87,14 +147,14 @@ func ReverifyStageFreight(
 		return fmt.Errorf("stage verification info has no ID")
 	}
 
-	kvs := map[string]*string{
-		AnnotationKeyReverify: ptr.To(curFreight.VerificationInfo.ID),
+	rr := VerificationRequest{
+		ID: curFreight.VerificationInfo.ID,
 	}
 	// Put actor information to track on the controller side
 	if u, ok := user.InfoFromContext(ctx); ok {
-		kvs[AnnotationKeyReverifyActor] = ptr.To(FormatEventUserActor(u))
+		rr.Actor = FormatEventUserActor(u)
 	}
-	return patchAnnotations(ctx, c, stage, kvs)
+	return patchAnnotation(ctx, c, stage, AnnotationKeyReverify, rr.String())
 }
 
 // AbortStageFreightVerification forces aborting the verification of the
@@ -131,5 +191,12 @@ func AbortStageFreightVerification(
 		return fmt.Errorf("stage verification info has no ID")
 	}
 
-	return patchAnnotation(ctx, c, stage, AnnotationKeyAbort, curFreight.VerificationInfo.ID)
+	ar := VerificationRequest{
+		ID: curFreight.VerificationInfo.ID,
+	}
+	// Put actor information to track on the controller side
+	if u, ok := user.InfoFromContext(ctx); ok {
+		ar.Actor = FormatEventUserActor(u)
+	}
+	return patchAnnotation(ctx, c, stage, AnnotationKeyAbort, ar.String())
 }
diff --git a/api/v1alpha1/stage_types.go b/api/v1alpha1/stage_types.go
index b62a85fe..20f519d5 100644
--- a/api/v1alpha1/stage_types.go
+++ b/api/v1alpha1/stage_types.go
@@ -504,6 +504,11 @@ type ArgoCDHelmImageUpdate struct {
 // StageStatus describes a Stages's current and recent Freight, health, and
 // more.
 type StageStatus struct {
+	// LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+	// annotation that was handled by the controller. This field can be used to
+	// determine whether the request to refresh the resource has been handled.
+	// +optional
+	LastHandledRefresh string `json:"lastHandledRefresh,omitempty" protobuf:"bytes,11,opt,name=lastHandledRefresh"`
 	// Phase describes where the Stage currently is in its lifecycle.
 	Phase StagePhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase"`
 	// CurrentFreight is a simplified representation of the Stage's current
@@ -728,6 +733,9 @@ type AnalysisRunArgument struct {
 type VerificationInfo struct {
 	// ID is the identifier of the Verification process.
 	ID string `json:"id,omitempty" protobuf:"bytes,4,opt,name=id"`
+	// Actor is the name of the entity that initiated or aborted the
+	// Verification process.
+	Actor string `json:"actor,omitempty" protobuf:"bytes,7,opt,name=actor"`
 	// StartTime is the time at which the Verification process was started.
 	StartTime *metav1.Time `json:"startTime,omitempty" protobuf:"bytes,5,opt,name=startTime"`
 	// Phase describes the current phase of the Verification process. Generally,
@@ -753,6 +761,14 @@ func (v *VerificationInfo) HasAnalysisRun() bool {
 
 type VerificationInfoStack []VerificationInfo
 
+// Current returns the VerificationInfo at the top of the stack.
+func (v *VerificationInfoStack) Current() *VerificationInfo {
+	if len(*v) == 0 {
+		return nil
+	}
+	return &(*v)[0]
+}
+
 // UpdateOrPush updates the VerificationInfo with the same ID as the provided
 // VerificationInfo or appends the provided VerificationInfo to the stack if no
 // such VerificationInfo is found.
diff --git a/api/v1alpha1/warehouse_types.go b/api/v1alpha1/warehouse_types.go
index 2ff2990d..d43dbf67 100644
--- a/api/v1alpha1/warehouse_types.go
+++ b/api/v1alpha1/warehouse_types.go
@@ -247,6 +247,11 @@ type ChartSubscription struct {
 
 // WarehouseStatus describes a Warehouse's most recently observed state.
 type WarehouseStatus struct {
+	// LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+	// annotation that was handled by the controller. This field can be used to
+	// determine whether the request to refresh the resource has been handled.
+	// +optional
+	LastHandledRefresh string `json:"lastHandledRefresh,omitempty" protobuf:"bytes,6,opt,name=lastHandledRefresh"`
 	// Message describes any errors that are preventing the Warehouse controller
 	// from polling repositories to discover new Freight.
 	Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"`
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
index 0da701b5..1eb4dab9 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
@@ -85,6 +85,12 @@ spec:
               Status describes the current state of the transition represented by this
               Promotion.
             properties:
+              lastHandledRefresh:
+                description: |-
+                  LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                  annotation that was handled by the controller. This field can be used to
+                  determine whether the request to refresh the resource has been handled.
+                type: string
               message:
                 description: |-
                   Message is a display message about the promotion, including any errors
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
index 60fafcff..2c3787d5 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
@@ -674,6 +674,11 @@ spec:
                         VerificationInfo contains information about the currently running
                         Verification process.
                       properties:
+                        actor:
+                          description: |-
+                            Actor is the name of the entity that initiated or aborted the
+                            Verification process.
+                          type: string
                         analysisRun:
                           description: |-
                             AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -726,6 +731,11 @@ spec:
                       VerificationInfo is information about any verification process that was
                       associated with this Freight for this Stage.
                     properties:
+                      actor:
+                        description: |-
+                          Actor is the name of the entity that initiated or aborted the
+                          Verification process.
+                        type: string
                       analysisRun:
                         description: |-
                           AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -894,6 +904,11 @@ spec:
                             VerificationInfo contains information about the currently running
                             Verification process.
                           properties:
+                            actor:
+                              description: |-
+                                Actor is the name of the entity that initiated or aborted the
+                                Verification process.
+                              type: string
                             analysisRun:
                               description: |-
                                 AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -947,6 +962,11 @@ spec:
                           VerificationInfo is information about any verification process that was
                           associated with this Freight for this Stage.
                         properties:
+                          actor:
+                            description: |-
+                              Actor is the name of the entity that initiated or aborted the
+                              Verification process.
+                            type: string
                           analysisRun:
                             description: |-
                               AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1005,6 +1025,12 @@ spec:
                   status:
                     description: Status is the (optional) status of the promotion
                     properties:
+                      lastHandledRefresh:
+                        description: |-
+                          LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                          annotation that was handled by the controller. This field can be used to
+                          determine whether the request to refresh the resource has been handled.
+                        type: string
                       message:
                         description: |-
                           Message is a display message about the promotion, including any errors
@@ -1204,6 +1230,11 @@ spec:
                           VerificationInfo contains information about the currently running
                           Verification process.
                         properties:
+                          actor:
+                            description: |-
+                              Actor is the name of the entity that initiated or aborted the
+                              Verification process.
+                            type: string
                           analysisRun:
                             description: |-
                               AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1257,6 +1288,11 @@ spec:
                         VerificationInfo is information about any verification process that was
                         associated with this Freight for this Stage.
                       properties:
+                        actor:
+                          description: |-
+                            Actor is the name of the entity that initiated or aborted the
+                            Verification process.
+                          type: string
                         analysisRun:
                           description: |-
                             AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1309,6 +1345,12 @@ spec:
                       type: string
                   type: object
                 type: array
+              lastHandledRefresh:
+                description: |-
+                  LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                  annotation that was handled by the controller. This field can be used to
+                  determine whether the request to refresh the resource has been handled.
+                type: string
               lastPromotion:
                 description: LastPromotion is a reference to the last completed promotion.
                 properties:
@@ -1425,6 +1467,11 @@ spec:
                             VerificationInfo contains information about the currently running
                             Verification process.
                           properties:
+                            actor:
+                              description: |-
+                                Actor is the name of the entity that initiated or aborted the
+                                Verification process.
+                              type: string
                             analysisRun:
                               description: |-
                                 AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1478,6 +1525,11 @@ spec:
                           VerificationInfo is information about any verification process that was
                           associated with this Freight for this Stage.
                         properties:
+                          actor:
+                            description: |-
+                              Actor is the name of the entity that initiated or aborted the
+                              Verification process.
+                            type: string
                           analysisRun:
                             description: |-
                               AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1536,6 +1588,12 @@ spec:
                   status:
                     description: Status is the (optional) status of the promotion
                     properties:
+                      lastHandledRefresh:
+                        description: |-
+                          LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                          annotation that was handled by the controller. This field can be used to
+                          determine whether the request to refresh the resource has been handled.
+                        type: string
                       message:
                         description: |-
                           Message is a display message about the promotion, including any errors
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml b/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
index e539a718..70aff895 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
@@ -400,6 +400,11 @@ spec:
                         VerificationInfo contains information about the currently running
                         Verification process.
                       properties:
+                        actor:
+                          description: |-
+                            Actor is the name of the entity that initiated or aborted the
+                            Verification process.
+                          type: string
                         analysisRun:
                           description: |-
                             AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -452,6 +457,11 @@ spec:
                       VerificationInfo is information about any verification process that was
                       associated with this Freight for this Stage.
                     properties:
+                      actor:
+                        description: |-
+                          Actor is the name of the entity that initiated or aborted the
+                          Verification process.
+                        type: string
                       analysisRun:
                         description: |-
                           AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -503,6 +513,12 @@ spec:
                       this Freight.
                     type: string
                 type: object
+              lastHandledRefresh:
+                description: |-
+                  LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                  annotation that was handled by the controller. This field can be used to
+                  determine whether the request to refresh the resource has been handled.
+                type: string
               message:
                 description: |-
                   Message describes any errors that are preventing the Warehouse controller
diff --git a/internal/cli/cmd/refresh/stage.go b/internal/cli/cmd/refresh/stage.go
index 879897d6..eb2ebfbf 100644
--- a/internal/cli/cmd/refresh/stage.go
+++ b/internal/cli/cmd/refresh/stage.go
@@ -81,8 +81,14 @@ func waitForStage(
 		if msg == nil || msg.Stage == nil {
 			return errors.New("unexpected response")
 		}
-		if msg.Stage.GetAnnotations() == nil ||
-			msg.Stage.GetAnnotations()[kargoapi.AnnotationKeyRefresh] == "" {
+		token, ok := kargoapi.RefreshAnnotationValue(msg.Stage.GetAnnotations())
+		if !ok {
+			return fmt.Errorf(
+				"Stage %q in Project %q has no %q annotation",
+				name, project, kargoapi.AnnotationKeyRefresh,
+			)
+		}
+		if msg.Stage.Status.LastHandledRefresh == token {
 			return nil
 		}
 	}
diff --git a/internal/cli/cmd/refresh/warehouse.go b/internal/cli/cmd/refresh/warehouse.go
index 48294019..d6202e3e 100644
--- a/internal/cli/cmd/refresh/warehouse.go
+++ b/internal/cli/cmd/refresh/warehouse.go
@@ -81,8 +81,14 @@ func waitForWarehouse(
 		if msg == nil || msg.Warehouse == nil {
 			return errors.New("unexpected response")
 		}
-		if msg.Warehouse.GetAnnotations() == nil ||
-			msg.Warehouse.GetAnnotations()[kargoapi.AnnotationKeyRefresh] == "" {
+		token, ok := kargoapi.RefreshAnnotationValue(msg.Warehouse.GetAnnotations())
+		if !ok {
+			return fmt.Errorf(
+				"Warehouse %q in Project %q has no %q annotation",
+				name, project, kargoapi.AnnotationKeyRefresh,
+			)
+		}
+		if msg.Warehouse.Status.LastHandledRefresh == token {
 			return nil
 		}
 	}
diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go
index b646750d..9ec9eff8 100644
--- a/internal/controller/promotions/promotions.go
+++ b/internal/controller/promotions/promotions.go
@@ -99,20 +99,13 @@ func SetupReconcilerWithManager(
 		cfg,
 	)
 
-	changePredicate := predicate.Or(
-		predicate.GenerationChangedPredicate{},
-		predicate.AnnotationChangedPredicate{},
-	)
-
 	c, err := ctrl.NewControllerManagedBy(kargoMgr).
 		For(&kargoapi.Promotion{}).
-		WithEventFilter(changePredicate).
+		WithEventFilter(predicate.Or(
+			predicate.GenerationChangedPredicate{},
+			kargo.RefreshRequested{},
+		)).
 		WithEventFilter(shardPredicate).
-		WithEventFilter(kargo.IgnoreAnnotationRemoval{
-			Annotations: []string{
-				kargoapi.AnnotationKeyRefresh,
-			},
-		}).
 		WithOptions(controller.CommonOptions()).
 		Build(reconciler)
 	if err != nil {
@@ -292,6 +285,11 @@ func (r *reconciler) Reconcile(
 		logger.Infof("promotion %s", newStatus.Phase)
 	}
 
+	// Record the current refresh token as having been handled.
+	if token, ok := kargoapi.RefreshAnnotationValue(promo.GetAnnotations()); ok {
+		newStatus.LastHandledRefresh = token
+	}
+
 	err = kubeclient.PatchStatus(ctx, r.kargoClient, promo, func(status *kargoapi.PromotionStatus) {
 		*status = *newStatus
 	})
@@ -346,15 +344,6 @@ func (r *reconciler) Reconcile(
 		r.recorder.AnnotatedEventf(promo, eventAnnotations, corev1.EventTypeNormal, reason, msg)
 	}
 
-	if clearRefreshErr := kargoapi.ClearAnnotations(
-		ctx,
-		r.kargoClient,
-		promo,
-		kargoapi.AnnotationKeyRefresh,
-	); clearRefreshErr != nil {
-		logger.Errorf("error clearing Promotion refresh annotation: %s", clearRefreshErr)
-	}
-
 	if err != nil {
 		// Controller runtime automatically gives us a progressive backoff if err is
 		// not nil
diff --git a/internal/controller/stages/stages.go b/internal/controller/stages/stages.go
index 6ada3b0d..2e7f4e06 100644
--- a/internal/controller/stages/stages.go
+++ b/internal/controller/stages/stages.go
@@ -2,7 +2,6 @@ package stages
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"sort"
 	"time"
@@ -316,18 +315,12 @@ func SetupReconcilerWithManager(
 		WithEventFilter(
 			predicate.Or(
 				predicate.GenerationChangedPredicate{},
-				predicate.AnnotationChangedPredicate{},
+				kargo.RefreshRequested{},
+				kargo.ReverifyRequested{},
+				kargo.AbortRequested{},
 			),
 		).
 		WithEventFilter(shardPredicate).
-		WithEventFilter(kargo.IgnoreAnnotationRemoval{
-			Annotations: []string{
-				kargoapi.AnnotationKeyRefresh,
-				kargoapi.AnnotationKeyReverify,
-				kargoapi.AnnotationKeyReverifyActor,
-				kargoapi.AnnotationKeyAbort,
-			},
-		}).
 		WithOptions(controller.CommonOptions()).
 		Build(
 			newReconciler(
@@ -553,30 +546,23 @@ func (r *reconciler) Reconcile(
 		newStatus.Message = ""
 	}
 
+	// Record the current refresh token as having been handled.
+	if token, ok := kargoapi.RefreshAnnotationValue(stage.GetAnnotations()); ok {
+		newStatus.LastHandledRefresh = token
+	}
+
 	updateErr := kubeclient.PatchStatus(ctx, r.kargoClient, stage, func(status *kargoapi.StageStatus) {
 		*status = newStatus
 	})
 	if updateErr != nil {
 		logger.Errorf("error updating Stage status: %s", updateErr)
 	}
-	clearErr := kargoapi.ClearAnnotations(
-		ctx,
-		r.kargoClient,
-		stage,
-		kargoapi.AnnotationKeyRefresh,
-		kargoapi.AnnotationKeyReverify,
-		kargoapi.AnnotationKeyReverifyActor,
-		kargoapi.AnnotationKeyAbort,
-	)
-	if clearErr != nil {
-		logger.Errorf("error clearing Stage annotations: %s", clearErr)
-	}
 
 	// If we had no error, but couldn't update, then we DO have an error. But we
 	// do it this way so that a failure to update is never counted as THE failure
 	// when something else more serious occurred first.
 	if err == nil {
-		err = errors.Join(updateErr, clearErr)
+		err = updateErr
 	}
 	logger.Debug("done reconciling Stage")
 
@@ -773,8 +759,8 @@ func (r *reconciler) syncNormalStage(
 			// Confirm if a reverification is requested. If so, clear the
 			// verification info to start the verification process again.
 			info := status.CurrentFreight.VerificationInfo
-			if info != nil && info.ID != "" && info.Phase.IsTerminal() {
-				if v, ok := stage.GetAnnotations()[kargoapi.AnnotationKeyReverify]; ok && v == info.ID {
+			if info != nil && info.Phase.IsTerminal() {
+				if req, _ := kargoapi.ReverifyAnnotationValue(stage.GetAnnotations()); req.ForID(info.ID) {
 					logger.Debug("rerunning verification")
 					status.Phase = kargoapi.StagePhaseVerifying
 					status.CurrentFreight.VerificationInfo = nil
@@ -816,8 +802,8 @@ func (r *reconciler) syncNormalStage(
 					// Abort the verification if it's still running and the Stage has
 					// been marked to do so.
 					newInfo := status.CurrentFreight.VerificationInfo
-					if newInfo.ID != "" && !newInfo.Phase.IsTerminal() {
-						if v, ok := stage.GetAnnotations()[kargoapi.AnnotationKeyAbort]; ok && v == newInfo.ID {
+					if !newInfo.Phase.IsTerminal() {
+						if req, _ := kargoapi.AbortAnnotationValue(stage.GetAnnotations()); req.ForID(newInfo.ID) {
 							log.Debug("aborting verification")
 							status.CurrentFreight.VerificationInfo = r.abortVerificationFn(ctx, stage)
 						}
@@ -1556,19 +1542,9 @@ func (r *reconciler) recordFreightVerificationEvent(
 	}
 
 	// If the verification is manually triggered (e.g. reverify),
-	// override the actor if the Stage or AnalysisRun knows about them.
-	{
-		// Check if the stage has reverify actor annotation
-		if actor, ok := s.Annotations[kargoapi.AnnotationKeyReverifyActor]; ok {
-			annotations[kargoapi.AnnotationKeyEventActor] = actor
-		}
-
-		// Or if the analysis run has it, then set it as the actor
-		if ar != nil {
-			if actor, ok := ar.Annotations[kargoapi.AnnotationKeyReverifyActor]; ok {
-				annotations[kargoapi.AnnotationKeyEventActor] = actor
-			}
-		}
+	// override the actor with the one who triggered the verification.
+	if vi.Actor != "" {
+		annotations[kargoapi.AnnotationKeyEventActor] = vi.Actor
 	}
 
 	reason := kargoapi.EventReasonFreightVerificationUnknown
diff --git a/internal/controller/stages/verification.go b/internal/controller/stages/verification.go
index 568ab230..9a08365f 100644
--- a/internal/controller/stages/verification.go
+++ b/internal/controller/stages/verification.go
@@ -34,23 +34,36 @@ func (r *reconciler) startVerification(
 	stage *kargoapi.Stage,
 ) (*kargoapi.VerificationInfo, error) {
 	startTime := r.nowFn()
+
+	newInfo := &kargoapi.VerificationInfo{
+		ID:        uuid.NewString(),
+		StartTime: ptr.To(metav1.NewTime(startTime)),
+	}
+
+	// Gather information about the verification being a re-verification request,
+	// and extract the actor who requested the re-verification to be used in the
+	// new verification info.
+	var isReverify bool
+	if curVer := stage.Status.CurrentFreight.VerificationHistory.Current(); curVer != nil {
+		if req, _ := kargoapi.ReverifyAnnotationValue(stage.GetAnnotations()); req.ForID(curVer.ID) {
+			isReverify = true
+			newInfo.Actor = req.Actor
+		}
+	}
+
 	if !r.cfg.RolloutsIntegrationEnabled {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: "Rollouts integration is disabled on this controller; " +
-				"cannot start verification",
-		}, nil
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = "Rollouts integration is disabled on this controller; cannot start verification"
+		return newInfo, nil
 	}
 
 	logger := logging.LoggerFromContext(ctx)
 
-	// If the stage does not have a reverification annotation, check if there is
-	// an existing AnalysisRun for the Stage and Freight. If there is, return
-	// the status of this AnalysisRun.
-	if _, ok := stage.GetAnnotations()[kargoapi.AnnotationKeyReverify]; !ok {
+	// If this is not a re-verification request, check if there is an existing
+	// AnalysisRun for the Stage and Freight. If there is, return the status of
+	// this AnalysisRun.
+	if !isReverify {
 		analysisRuns := rollouts.AnalysisRunList{}
 		if err := r.listAnalysisRunsFn(
 			ctx,
@@ -65,19 +78,16 @@ func (r *reconciler) startVerification(
 				),
 			},
 		); err != nil {
-			return &kargoapi.VerificationInfo{
-				ID:         uuid.NewString(),
-				StartTime:  ptr.To(metav1.NewTime(startTime)),
-				FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-				Phase:      kargoapi.VerificationPhaseError,
-				Message: fmt.Errorf(
-					"error listing AnalysisRuns for Stage %q and Freight %q in namespace %q: %w",
-					stage.Name,
-					stage.Status.CurrentFreight.Name,
-					stage.Namespace,
-					err,
-				).Error(),
-			}, err
+			newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+			newInfo.Phase = kargoapi.VerificationPhaseError
+			newInfo.Message = fmt.Errorf(
+				"error listing AnalysisRuns for Stage %q and Freight %q in namespace %q: %w",
+				stage.Name,
+				stage.Status.CurrentFreight.Name,
+				stage.Namespace,
+				err,
+			).Error()
+			return newInfo, err
 		}
 		if len(analysisRuns.Items) > 0 {
 			// Sort the AnalysisRuns by creation timestamp, so that the most recent
@@ -88,17 +98,15 @@ func (r *reconciler) startVerification(
 
 			logger.Debug("AnalysisRun already exists for Freight")
 			latestAnalysisRun := analysisRuns.Items[0]
-			return &kargoapi.VerificationInfo{
-				ID:         uuid.NewString(),
-				StartTime:  ptr.To(latestAnalysisRun.CreationTimestamp),
-				FinishTime: latestAnalysisRun.Status.CompletedAt(),
-				Phase:      kargoapi.VerificationPhase(latestAnalysisRun.Status.Phase),
-				AnalysisRun: &kargoapi.AnalysisRunReference{
-					Name:      latestAnalysisRun.Name,
-					Namespace: latestAnalysisRun.Namespace,
-					Phase:     string(latestAnalysisRun.Status.Phase),
-				},
-			}, nil
+
+			newInfo.FinishTime = latestAnalysisRun.Status.CompletedAt()
+			newInfo.Phase = kargoapi.VerificationPhase(latestAnalysisRun.Status.Phase)
+			newInfo.AnalysisRun = &kargoapi.AnalysisRunReference{
+				Name:      latestAnalysisRun.Name,
+				Namespace: latestAnalysisRun.Namespace,
+				Phase:     string(latestAnalysisRun.Status.Phase),
+			}
+			return newInfo, nil
 		}
 	}
 
@@ -115,31 +123,25 @@ func (r *reconciler) startVerification(
 			},
 		)
 		if err != nil {
-			return &kargoapi.VerificationInfo{
-				ID:         uuid.NewString(),
-				StartTime:  ptr.To(metav1.NewTime(startTime)),
-				FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-				Phase:      kargoapi.VerificationPhaseError,
-				Message: fmt.Errorf(
-					"error getting AnalysisTemplate %q in namespace %q: %w",
-					templateRef.Name,
-					stage.Namespace,
-					err,
-				).Error(),
-			}, err
+			newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+			newInfo.Phase = kargoapi.VerificationPhaseError
+			newInfo.Message = fmt.Errorf(
+				"error getting AnalysisTemplate %q in namespace %q: %w",
+				templateRef.Name,
+				stage.Namespace,
+				err,
+			).Error()
+			return newInfo, err
 		}
 		if template == nil {
-			return &kargoapi.VerificationInfo{
-				ID:         uuid.NewString(),
-				StartTime:  ptr.To(metav1.NewTime(startTime)),
-				FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-				Phase:      kargoapi.VerificationPhaseError,
-				Message: fmt.Errorf(
-					"AnalysisTemplate %q in namespace %q not found",
-					templateRef.Name,
-					stage.Namespace,
-				).Error(),
-			}, nil
+			newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+			newInfo.Phase = kargoapi.VerificationPhaseError
+			newInfo.Message = fmt.Errorf(
+				"AnalysisTemplate %q in namespace %q not found",
+				templateRef.Name,
+				stage.Namespace,
+			).Error()
+			return newInfo, nil
 		}
 		templates[i] = template
 	}
@@ -153,74 +155,60 @@ func (r *reconciler) startVerification(
 		},
 	)
 	if err != nil {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"error getting Freight %q in namespace %q: %w",
-				stage.Status.CurrentFreight.Name,
-				stage.Namespace,
-				err,
-			).Error(),
-		}, err
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"error getting Freight %q in namespace %q: %w",
+			stage.Status.CurrentFreight.Name,
+			stage.Namespace,
+			err,
+		).Error()
+		return newInfo, err
 	}
 	if freight == nil {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"Freight %q in namespace %q not found",
-				stage.Status.CurrentFreight.Name,
-				stage.Namespace,
-			).Error(),
-		}, nil
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"Freight %q in namespace %q not found",
+			stage.Status.CurrentFreight.Name,
+			stage.Namespace,
+		).Error()
+		return newInfo, nil
 	}
 
 	run, err := r.buildAnalysisRunFn(stage, freight, templates)
 	if err != nil {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"error building AnalysisRun for Stage %q and Freight %q in namespace %q: %w",
-				stage.Name,
-				stage.Status.CurrentFreight.Name,
-				stage.Namespace,
-				err,
-			).Error(),
-		}, nil
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"error building AnalysisRun for Stage %q and Freight %q in namespace %q: %w",
+			stage.Name,
+			stage.Status.CurrentFreight.Name,
+			stage.Namespace,
+			err,
+		).Error()
+		return newInfo, nil
 	}
 
 	if err := r.createAnalysisRunFn(ctx, run); err != nil {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"error creating AnalysisRun %q in namespace %q: %w",
-				run.Name,
-				run.Namespace,
-				err,
-			).Error(),
-		}, kubeclient.IgnoreInvalid(err) // Ignore errors which are due to validation issues
-	}
-
-	return &kargoapi.VerificationInfo{
-		ID:        uuid.NewString(),
-		StartTime: ptr.To(run.CreationTimestamp),
-		Phase:     kargoapi.VerificationPhasePending,
-		AnalysisRun: &kargoapi.AnalysisRunReference{
-			Name:      run.Name,
-			Namespace: run.Namespace,
-		},
-	}, nil
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"error creating AnalysisRun %q in namespace %q: %w",
+			run.Name,
+			run.Namespace,
+			err,
+		).Error()
+		return newInfo, kubeclient.IgnoreInvalid(err) // Ignore errors which are due to validation issues
+	}
+
+	newInfo.StartTime = ptr.To(run.CreationTimestamp)
+	newInfo.Phase = kargoapi.VerificationPhasePending
+	newInfo.AnalysisRun = &kargoapi.AnalysisRunReference{
+		Name:      run.Name,
+		Namespace: run.Namespace,
+	}
+	return newInfo, nil
 }
 
 // getVerificationInfo returns the status of the AnalysisRun for the given Stage.
@@ -239,6 +227,7 @@ func (r *reconciler) getVerificationInfo(
 	if !r.cfg.RolloutsIntegrationEnabled {
 		return &kargoapi.VerificationInfo{
 			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
+			Actor:      stage.Status.CurrentFreight.VerificationInfo.Actor,
 			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
 			FinishTime: stage.Status.CurrentFreight.VerificationInfo.FinishTime,
 			Phase:      kargoapi.VerificationPhaseError,
@@ -259,6 +248,7 @@ func (r *reconciler) getVerificationInfo(
 	if err != nil {
 		return &kargoapi.VerificationInfo{
 			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
+			Actor:      stage.Status.CurrentFreight.VerificationInfo.Actor,
 			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
 			FinishTime: stage.Status.CurrentFreight.VerificationInfo.FinishTime,
 			Phase:      kargoapi.VerificationPhaseError,
@@ -274,6 +264,7 @@ func (r *reconciler) getVerificationInfo(
 	if analysisRun == nil {
 		return &kargoapi.VerificationInfo{
 			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
+			Actor:      stage.Status.CurrentFreight.VerificationInfo.Actor,
 			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
 			FinishTime: stage.Status.CurrentFreight.VerificationInfo.FinishTime,
 			Phase:      kargoapi.VerificationPhaseError,
@@ -287,6 +278,7 @@ func (r *reconciler) getVerificationInfo(
 
 	return &kargoapi.VerificationInfo{
 		ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
+		Actor:      stage.Status.CurrentFreight.VerificationInfo.Actor,
 		StartTime:  ptr.To(analysisRun.CreationTimestamp),
 		FinishTime: analysisRun.Status.CompletedAt(),
 		Phase:      kargoapi.VerificationPhase(analysisRun.Status.Phase),
@@ -302,15 +294,23 @@ func (r *reconciler) abortVerification(
 	ctx context.Context,
 	stage *kargoapi.Stage,
 ) *kargoapi.VerificationInfo {
+	currentInfo := stage.Status.CurrentFreight.VerificationInfo
+	newInfo := &kargoapi.VerificationInfo{
+		ID:        currentInfo.ID,
+		StartTime: currentInfo.StartTime,
+	}
+
+	// Extract the actor who requested the abort to be used in the new
+	// verification info.
+	if req, _ := kargoapi.AbortAnnotationValue(stage.GetAnnotations()); req.ForID(currentInfo.ID) {
+		newInfo.Actor = req.Actor
+	}
+
 	if !r.cfg.RolloutsIntegrationEnabled {
-		return &kargoapi.VerificationInfo{
-			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
-			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: "Rollouts integration is disabled on this controller; cannot " +
-				"abort verification",
-		}
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = "Rollouts integration is disabled on this controller; cannot abort verification"
+		return newInfo
 	}
 
 	ar := &rollouts.AnalysisRun{
@@ -324,19 +324,16 @@ func (r *reconciler) abortVerification(
 		ar,
 		client.RawPatch(types.MergePatchType, []byte(`{"spec":{"terminate":true}}`)),
 	); err != nil {
-		return &kargoapi.VerificationInfo{
-			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
-			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"error terminating AnalysisRun %q in namespace %q: %w",
-				ar.Name,
-				ar.Namespace,
-				err,
-			).Error(),
-			AnalysisRun: stage.Status.CurrentFreight.VerificationInfo.AnalysisRun.DeepCopy(),
-		}
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"error terminating AnalysisRun %q in namespace %q: %w",
+			ar.Name,
+			ar.Namespace,
+			err,
+		).Error()
+		newInfo.AnalysisRun = currentInfo.AnalysisRun.DeepCopy()
+		return newInfo
 	}
 
 	// Return a new VerificationInfo with the same ID and a message indicating
@@ -344,14 +341,12 @@ func (r *reconciler) abortVerification(
 	// the verification was not successful.
 	// We do not use the further information from the AnalysisRun, as this
 	// will indicate a "Succeeded" phase due to Argo Rollouts behavior.
-	return &kargoapi.VerificationInfo{
-		ID:          stage.Status.CurrentFreight.VerificationInfo.ID,
-		StartTime:   ptr.To(ar.CreationTimestamp),
-		FinishTime:  ptr.To(metav1.NewTime(r.nowFn())),
-		Phase:       kargoapi.VerificationPhaseAborted,
-		Message:     "Verification aborted by user",
-		AnalysisRun: stage.Status.CurrentFreight.VerificationInfo.AnalysisRun.DeepCopy(),
-	}
+	newInfo.StartTime = ptr.To(ar.CreationTimestamp)
+	newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+	newInfo.Phase = kargoapi.VerificationPhaseAborted
+	newInfo.Message = "Verification aborted by user"
+	newInfo.AnalysisRun = currentInfo.AnalysisRun.DeepCopy()
+	return newInfo
 }
 
 func (r *reconciler) buildAnalysisRun(
@@ -396,13 +391,13 @@ func (r *reconciler) buildAnalysisRun(
 	lbls[kargoapi.StageLabelKey] = stage.Name
 	lbls[kargoapi.FreightLabelKey] = stage.Status.CurrentFreight.Name
 
-	// Check if the AnalysisRun is triggered manually.
-	// When the promotion controller requests re-verification for the re-promotion,
-	// there should be no Reverify Actor key in annotations. However, if the re-verification
-	// is requested by the user, the Reverify Actor key should be added to annotations.
-	if actor, ok := stage.GetAnnotations()[kargoapi.AnnotationKeyReverifyActor]; ok {
-		annotations[kargoapi.AnnotationKeyReverifyActor] = actor
-	} else {
+	// Add Promotion name if the AnalysisRun is triggered by Promotion.
+	// This is the case when there is no existing verification information,
+	// or the re-verification request has been made by the control plane
+	// without an actor specified.
+	curInfo := stage.Status.CurrentFreight.VerificationHistory.Current()
+	reverifyReq, _ := kargoapi.ReverifyAnnotationValue(stage.GetAnnotations())
+	if curInfo == nil || reverifyReq.ForID(curInfo.ID) && reverifyReq.ControlPlane && reverifyReq.Actor == "" {
 		// Add Promotion name if the AnalysisRun is triggered by Promotion.
 		if stage.Status.LastPromotion != nil {
 			lbls[kargoapi.PromotionLabelKey] = stage.Status.LastPromotion.Name
diff --git a/internal/controller/warehouses/warehouses.go b/internal/controller/warehouses/warehouses.go
index 775d9b15..8cb073c7 100644
--- a/internal/controller/warehouses/warehouses.go
+++ b/internal/controller/warehouses/warehouses.go
@@ -119,15 +119,10 @@ func SetupReconcilerWithManager(
 		WithEventFilter(
 			predicate.Or(
 				predicate.GenerationChangedPredicate{},
-				predicate.AnnotationChangedPredicate{},
+				kargo.RefreshRequested{},
 			),
 		).
 		WithEventFilter(shardPredicate).
-		WithEventFilter(kargo.IgnoreAnnotationRemoval{
-			Annotations: []string{
-				kargoapi.AnnotationKeyRefresh,
-			},
-		}).
 		WithOptions(controller.CommonOptions()).
 		Complete(newReconciler(mgr.GetClient(), credentialsDB)); err != nil {
 		return fmt.Errorf("error building Warehouse reconciler: %w", err)
@@ -204,14 +199,6 @@ func (r *reconciler) Reconcile(
 	if updateErr != nil {
 		logger.Errorf("error updating Warehouse status: %s", updateErr)
 	}
-	if clearRefreshErr := kargoapi.ClearAnnotations(
-		ctx,
-		r.client,
-		warehouse,
-		kargoapi.AnnotationKeyRefresh,
-	); clearRefreshErr != nil {
-		logger.Errorf("error clearing Warehouse refresh annotation: %s", clearRefreshErr)
-	}
 
 	// If we had no error, but couldn't update, then we DO have an error. But we
 	// do it this way so that a failure to update is never counted as THE failure
@@ -241,6 +228,11 @@ func (r *reconciler) syncWarehouse(
 	status.ObservedGeneration = warehouse.Generation
 	status.Message = "" // Clear any previous error
 
+	// Record the current refresh token as having been handled.
+	if token, ok := kargoapi.RefreshAnnotationValue(warehouse.GetAnnotations()); ok {
+		status.LastHandledRefresh = token
+	}
+
 	logger := logging.LoggerFromContext(ctx)
 
 	freight, err := r.getLatestFreightFromReposFn(ctx, warehouse)
diff --git a/internal/kargo/kargo.go b/internal/kargo/kargo.go
index 0b3816da..68c32dd1 100644
--- a/internal/kargo/kargo.go
+++ b/internal/kargo/kargo.go
@@ -106,34 +106,71 @@ func (p PromoWentTerminal) Update(e event.UpdateEvent) bool {
 	return false
 }
 
-// IgnoreAnnotationRemoval is a predicate that ignores the removal of specific
-// Annotations in an update event. This is useful when we want to ignore
-// updates that only remove an annotation and nothing else, which may be the
-// case when the annotation(s) are cleared by the controller itself.
-type IgnoreAnnotationRemoval struct {
+// RefreshRequested is a predicate that returns true if the refresh annotation
+// has been set on a resource, or the value of the annotation has changed
+// compared to the previous state.
+type RefreshRequested struct {
 	predicate.Funcs
+}
+
+// Update returns true if the refresh annotation has been set on the new object,
+// or if the value of the annotation has changed compared to the old object.
+func (p RefreshRequested) Update(e event.UpdateEvent) bool {
+	if e.ObjectOld == nil || e.ObjectNew == nil {
+		return false
+	}
 
-	Annotations []string
+	if newVal, newOk := kargoapi.RefreshAnnotationValue(e.ObjectNew.GetAnnotations()); newOk {
+		if oldVal, oldOk := kargoapi.RefreshAnnotationValue(e.ObjectOld.GetAnnotations()); oldOk {
+			return newVal != oldVal
+		}
+		return true
+	}
+	return false
+}
+
+// ReverifyRequested is a predicate that returns true if the reverify annotation
+// has been set on a resource, or the ID of the request has changed compared to
+// the previous state.
+type ReverifyRequested struct {
+	predicate.Funcs
 }
 
-// Update returns false if the update event only removed any of the Annotations,
-// true otherwise.
-//
-// NOTE: this predicate assumes that the update was to clear the annotation and
-// nothing else. This is safe to assume as long as the helpers of the API are
-// always used to clear the respective annotation.
-func (i IgnoreAnnotationRemoval) Update(e event.UpdateEvent) bool {
+// Update returns true if the reverify annotation has been set on the new object,
+// or if the ID of the request has changed compared to the old object.
+func (r ReverifyRequested) Update(e event.UpdateEvent) bool {
 	if e.ObjectOld == nil || e.ObjectNew == nil {
+		return false
+	}
+
+	if newVal, newOk := kargoapi.ReverifyAnnotationValue(e.ObjectNew.GetAnnotations()); newOk {
+		if oldVal, oldOk := kargoapi.ReverifyAnnotationValue(e.ObjectOld.GetAnnotations()); oldOk {
+			return !newVal.ForID(oldVal.ID)
+		}
 		return true
 	}
-	oldAnnotations := e.ObjectOld.GetAnnotations()
-	newAnnotations := e.ObjectNew.GetAnnotations()
-	for _, annotation := range i.Annotations {
-		_, oldHasAnnotation := oldAnnotations[annotation]
-		_, newHasAnnotation := newAnnotations[annotation]
-		if oldHasAnnotation && !newHasAnnotation {
-			return false
+	return false
+}
+
+// AbortRequested is a predicate that returns true if the abort annotation has
+// been set on a resource, or the ID of the request has changed compared to the
+// previous state.
+type AbortRequested struct {
+	predicate.Funcs
+}
+
+// Update returns true if the abort annotation has been set on the new object,
+// or if the ID of the request has changed compared to the old object.
+func (p AbortRequested) Update(e event.UpdateEvent) bool {
+	if e.ObjectOld == nil || e.ObjectNew == nil {
+		return false
+	}
+
+	if newVal, newOk := kargoapi.AbortAnnotationValue(e.ObjectNew.GetAnnotations()); newOk {
+		if oldVal, oldOk := kargoapi.AbortAnnotationValue(e.ObjectOld.GetAnnotations()); oldOk {
+			return !newVal.ForID(oldVal.ID)
 		}
+		return true
 	}
-	return true
+	return false
 }
diff --git a/internal/webhook/stage/webhook.go b/internal/webhook/stage/webhook.go
index 230578dc..1da05ea5 100644
--- a/internal/webhook/stage/webhook.go
+++ b/internal/webhook/stage/webhook.go
@@ -96,20 +96,43 @@ func (w *webhook) Default(ctx context.Context, obj runtime.Object) error {
 		oldStage = req.OldObject.Object.(*kargoapi.Stage) // nolint: forcetypeassert
 	}
 
-	if req.Operation == admissionv1.Create ||
-		req.Operation == admissionv1.Update {
-		if id, ok := stage.Annotations[kargoapi.AnnotationKeyReverify]; ok {
-			// Set actor as an admission request's user info when reverification is requested
-			// to allow controllers to track who triggered it.
-			if !w.isRequestFromKargoControlplaneFn(req) &&
-				(oldStage == nil ||
-					(oldStage != nil && oldStage.Annotations[kargoapi.AnnotationKeyReverify] != id)) {
-				stage.Annotations[kargoapi.AnnotationKeyReverifyActor] =
-					kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
+	if req.Operation == admissionv1.Create || req.Operation == admissionv1.Update {
+		if verReq, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations); ok {
+			var oldVerReq *kargoapi.VerificationRequest
+			if oldStage != nil {
+				oldVerReq, _ = kargoapi.ReverifyAnnotationValue(oldStage.Annotations)
+			}
+			// If the re-verification request has changed, enrich the annotation
+			// with the actor and control plane information.
+			if oldStage == nil || oldVerReq == nil || !verReq.Equals(oldVerReq) {
+				verReq.ControlPlane = w.isRequestFromKargoControlplaneFn(req)
+				if !verReq.ControlPlane {
+					// If the re-verification request is not from the control plane, then
+					// it's from a specific Kubernetes user. Without this check we would
+					// overwrite the actor field set by the control plane.
+					verReq.Actor = kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
+				}
+				stage.Annotations[kargoapi.AnnotationKeyReverify] = verReq.String()
+			}
+		}
+
+		if verReq, ok := kargoapi.AbortAnnotationValue(stage.Annotations); ok {
+			var oldVerReq *kargoapi.VerificationRequest
+			if oldStage != nil {
+				oldVerReq, _ = kargoapi.AbortAnnotationValue(oldStage.Annotations)
+			}
+			// If the abort request has changed, enrich the annotation with the
+			// actor and control plane information.
+			if oldStage == nil || oldVerReq == nil || !verReq.Equals(oldVerReq) {
+				verReq.ControlPlane = w.isRequestFromKargoControlplaneFn(req)
+				if !verReq.ControlPlane {
+					// If the abort request is not from the control plane, then
+					// it's from a specific Kubernetes user. Without this check we would
+					// overwrite the actor field set by the control plane.
+					verReq.Actor = kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
+				}
+				stage.Annotations[kargoapi.AnnotationKeyAbort] = verReq.String()
 			}
-		} else {
-			// Ensure actor annotation is not set when not reverifying
-			delete(stage.Annotations, kargoapi.AnnotationKeyReverifyActor)
 		}
 	}
 	return nil
diff --git a/ui/src/features/project/pipelines/pipelines.tsx b/ui/src/features/project/pipelines/pipelines.tsx
index f7451096..a8b50374 100644
--- a/ui/src/features/project/pipelines/pipelines.tsx
+++ b/ui/src/features/project/pipelines/pipelines.tsx
@@ -196,7 +196,9 @@ export const Pipelines = () => {
           }
         }
 
-        const refreshing = e.warehouse?.metadata?.annotations['kargo.akuity.io/refresh'];
+        const refreshRequest = e.warehouse?.metadata?.annotations['kargo.akuity.io/refresh'];
+        const refreshStatus = e.warehouse?.status?.lastHandledRefresh;
+        const refreshing = refreshRequest !== undefined && refreshRequest !== refreshStatus;
         if (refreshing) {
           refresh[e.warehouse?.metadata?.name || ''] = true;
         } else if (refresh[e.warehouse?.metadata?.name || '']) {
diff --git a/ui/src/features/project/pipelines/types.ts b/ui/src/features/project/pipelines/types.ts
index 57746934..c9ba45c6 100644
--- a/ui/src/features/project/pipelines/types.ts
+++ b/ui/src/features/project/pipelines/types.ts
@@ -50,11 +50,13 @@ export type NodesItemType =
 
 export const NewWarehouseNode = (warehouse: Warehouse, stageNames?: string[]): NodesRepoType => {
   const name = warehouse?.metadata?.name || '';
+  const refreshRequest = warehouse?.metadata?.annotations['kargo.akuity.io/refresh'];
+  const refreshStatus = warehouse?.status?.refreshStatus?.lastHandledRefresh;
   return {
     data: warehouse,
     stageNames: stageNames || [],
     warehouseName: name,
-    refreshing: !!warehouse?.metadata?.annotations['kargo.akuity.io/refresh'],
+    refreshing: refreshRequest !== undefined && refreshRequest !== refreshStatus,
     type: NodeType.WAREHOUSE
   };
 };
diff --git a/ui/src/features/stage/stage-actions.tsx b/ui/src/features/stage/stage-actions.tsx
index 12d3aa30..787580e2 100644
--- a/ui/src/features/stage/stage-actions.tsx
+++ b/ui/src/features/stage/stage-actions.tsx
@@ -58,11 +58,13 @@ export const StageActions = ({ stage }: { stage: Stage }) => {
 
   // Once the Refresh process is done, refetch Freight list
   React.useEffect(() => {
-    if (stage?.metadata?.annotations['kargo.akuity.io/refresh']) {
+    const refreshRequest = stage?.metadata?.annotations['kargo.akuity.io/refresh'];
+    const refreshStatus = stage?.status?.lastHandledRefresh;
+    if (refreshRequest !== undefined && refreshRequest !== refreshStatus) {
       setShouldRefetchFreights(true);
     }
 
-    if (!stage?.metadata?.annotations['kargo.akuity.io/refresh'] && shouldRefetchFreights) {
+    if (refreshRequest === refreshStatus && shouldRefetchFreights) {
       queryClient.invalidateQueries({ queryKey: createConnectQueryKey(queryFreight) });
       setShouldRefetchFreights(false);
     }
@@ -128,7 +130,7 @@ export const StageActions = ({ stage }: { stage: Stage }) => {
         type='default'
         icon={<FontAwesomeIcon icon={faRefresh} size='1x' />}
         onClick={onRefresh}
-        loading={isRefreshLoading || !!stage?.metadata?.annotations['kargo.akuity.io/refresh']}
+        loading={isRefreshLoading || (!!stage?.metadata?.annotations['kargo.akuity.io/refresh'] && stage?.metadata?.annotations?.['kargo.akuity.io/refresh'] !== stage?.status?.lastHandledRefresh)}
       >
         Refresh
       </Button>
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 3a096a96..dcdcb31e 100644
--- a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
@@ -37,6 +37,10 @@
     "status": {
       "description": "Status describes the current state of the transition represented by this\nPromotion.",
       "properties": {
+        "lastHandledRefresh": {
+          "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+          "type": "string"
+        },
         "message": {
           "description": "Message is a display message about the promotion, including any errors\npreventing the Promotion controller from executing this Promotion.\ni.e. If the Phase field has a value of Failed, this field can be expected\nto explain why.",
           "type": "string"
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 ae5a51ca..56462368 100644
--- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
@@ -550,6 +550,10 @@
               "items": {
                 "description": "VerificationInfo contains information about the currently running\nVerification process.",
                 "properties": {
+                  "actor": {
+                    "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                    "type": "string"
+                  },
                   "analysisRun": {
                     "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                     "properties": {
@@ -603,6 +607,10 @@
             "verificationInfo": {
               "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
               "properties": {
+                "actor": {
+                  "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                  "type": "string"
+                },
                 "analysisRun": {
                   "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                   "properties": {
@@ -759,6 +767,10 @@
                   "items": {
                     "description": "VerificationInfo contains information about the currently running\nVerification process.",
                     "properties": {
+                      "actor": {
+                        "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                        "type": "string"
+                      },
                       "analysisRun": {
                         "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                         "properties": {
@@ -812,6 +824,10 @@
                 "verificationInfo": {
                   "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
                   "properties": {
+                    "actor": {
+                      "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                      "type": "string"
+                    },
                     "analysisRun": {
                       "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                       "properties": {
@@ -874,6 +890,10 @@
             "status": {
               "description": "Status is the (optional) status of the promotion",
               "properties": {
+                "lastHandledRefresh": {
+                  "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+                  "type": "string"
+                },
                 "message": {
                   "description": "Message is a display message about the promotion, including any errors\npreventing the Promotion controller from executing this Promotion.\ni.e. If the Phase field has a value of Failed, this field can be expected\nto explain why.",
                   "type": "string"
@@ -1074,6 +1094,10 @@
                 "items": {
                   "description": "VerificationInfo contains information about the currently running\nVerification process.",
                   "properties": {
+                    "actor": {
+                      "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                      "type": "string"
+                    },
                     "analysisRun": {
                       "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                       "properties": {
@@ -1127,6 +1151,10 @@
               "verificationInfo": {
                 "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
                 "properties": {
+                  "actor": {
+                    "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                    "type": "string"
+                  },
                   "analysisRun": {
                     "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                     "properties": {
@@ -1184,6 +1212,10 @@
           },
           "type": "array"
         },
+        "lastHandledRefresh": {
+          "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+          "type": "string"
+        },
         "lastPromotion": {
           "description": "LastPromotion is a reference to the last completed promotion.",
           "properties": {
@@ -1285,6 +1317,10 @@
                   "items": {
                     "description": "VerificationInfo contains information about the currently running\nVerification process.",
                     "properties": {
+                      "actor": {
+                        "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                        "type": "string"
+                      },
                       "analysisRun": {
                         "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                         "properties": {
@@ -1338,6 +1374,10 @@
                 "verificationInfo": {
                   "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
                   "properties": {
+                    "actor": {
+                      "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                      "type": "string"
+                    },
                     "analysisRun": {
                       "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                       "properties": {
@@ -1400,6 +1440,10 @@
             "status": {
               "description": "Status is the (optional) status of the promotion",
               "properties": {
+                "lastHandledRefresh": {
+                  "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+                  "type": "string"
+                },
                 "message": {
                   "description": "Message is a display message about the promotion, including any errors\npreventing the Promotion controller from executing this Promotion.\ni.e. If the Phase field has a value of Failed, this field can be expected\nto explain why.",
                   "type": "string"
diff --git a/ui/src/gen/schema/warehouses.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/warehouses.kargo.akuity.io_v1alpha1.json
index 177b42e4..b5d8816f 100644
--- a/ui/src/gen/schema/warehouses.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/warehouses.kargo.akuity.io_v1alpha1.json
@@ -280,6 +280,10 @@
               "items": {
                 "description": "VerificationInfo contains information about the currently running\nVerification process.",
                 "properties": {
+                  "actor": {
+                    "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                    "type": "string"
+                  },
                   "analysisRun": {
                     "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                     "properties": {
@@ -333,6 +337,10 @@
             "verificationInfo": {
               "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
               "properties": {
+                "actor": {
+                  "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                  "type": "string"
+                },
                 "analysisRun": {
                   "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                   "properties": {
@@ -388,6 +396,10 @@
           },
           "type": "object"
         },
+        "lastHandledRefresh": {
+          "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+          "type": "string"
+        },
         "message": {
           "description": "Message describes any errors that are preventing the Warehouse controller\nfrom polling repositories to discover new Freight.",
           "type": "string"
diff --git a/ui/src/gen/v1alpha1/generated_pb.ts b/ui/src/gen/v1alpha1/generated_pb.ts
index e6b7ae24..4a724d57 100644
--- a/ui/src/gen/v1alpha1/generated_pb.ts
+++ b/ui/src/gen/v1alpha1/generated_pb.ts
@@ -2887,6 +2887,16 @@ export class PromotionSpec extends Message<PromotionSpec> {
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionStatus
  */
 export class PromotionStatus extends Message<PromotionStatus> {
+  /**
+   * LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+   * annotation that was handled by the controller. This field can be used to
+   * determine whether the request to refresh the resource has been handled.
+   * +optional
+   *
+   * @generated from field: optional string lastHandledRefresh = 4;
+   */
+  lastHandledRefresh?: string;
+
   /**
    * Phase describes where the Promotion currently is in its lifecycle.
    *
@@ -2920,6 +2930,7 @@ export class PromotionStatus extends Message<PromotionStatus> {
   static readonly runtime: typeof proto2 = proto2;
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.PromotionStatus";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
+    { no: 4, name: "lastHandledRefresh", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 1, name: "phase", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 2, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 3, name: "metadata", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} },
@@ -3239,6 +3250,16 @@ export class StageSpec extends Message<StageSpec> {
  * @generated from message github.com.akuity.kargo.api.v1alpha1.StageStatus
  */
 export class StageStatus extends Message<StageStatus> {
+  /**
+   * LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+   * annotation that was handled by the controller. This field can be used to
+   * determine whether the request to refresh the resource has been handled.
+   * +optional
+   *
+   * @generated from field: optional string lastHandledRefresh = 11;
+   */
+  lastHandledRefresh?: string;
+
   /**
    * Phase describes where the Stage currently is in its lifecycle.
    *
@@ -3307,6 +3328,7 @@ export class StageStatus extends Message<StageStatus> {
   static readonly runtime: typeof proto2 = proto2;
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.StageStatus";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
+    { no: 11, name: "lastHandledRefresh", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 1, name: "phase", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 2, name: "currentFreight", kind: "message", T: FreightReference, opt: true },
     { no: 3, name: "history", kind: "message", T: FreightReference, repeated: true },
@@ -3504,6 +3526,14 @@ export class VerificationInfo extends Message<VerificationInfo> {
    */
   id?: string;
 
+  /**
+   * Actor is the name of the entity that initiated or aborted the
+   * Verification process.
+   *
+   * @generated from field: optional string actor = 7;
+   */
+  actor?: string;
+
   /**
    * StartTime is the time at which the Verification process was started.
    *
@@ -3553,6 +3583,7 @@ export class VerificationInfo extends Message<VerificationInfo> {
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.VerificationInfo";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
     { no: 4, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
+    { no: 7, name: "actor", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 5, name: "startTime", kind: "message", T: Time, opt: true },
     { no: 1, name: "phase", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 2, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
@@ -3778,6 +3809,16 @@ export class WarehouseSpec extends Message<WarehouseSpec> {
  * @generated from message github.com.akuity.kargo.api.v1alpha1.WarehouseStatus
  */
 export class WarehouseStatus extends Message<WarehouseStatus> {
+  /**
+   * LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+   * annotation that was handled by the controller. This field can be used to
+   * determine whether the request to refresh the resource has been handled.
+   * +optional
+   *
+   * @generated from field: optional string lastHandledRefresh = 6;
+   */
+  lastHandledRefresh?: string;
+
   /**
    * Message describes any errors that are preventing the Warehouse controller
    * from polling repositories to discover new Freight.
@@ -3809,6 +3850,7 @@ export class WarehouseStatus extends Message<WarehouseStatus> {
   static readonly runtime: typeof proto2 = proto2;
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.WarehouseStatus";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
+    { no: 6, name: "lastHandledRefresh", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 3, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 4, name: "observedGeneration", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true },
     { no: 5, name: "lastFreight", kind: "message", T: FreightReference, opt: true },


Test patch:
diff --git a/api/v1alpha1/annotations_test.go b/api/v1alpha1/annotations_test.go
new file mode 100644
index 00000000..6215e3f9
--- /dev/null
+++ b/api/v1alpha1/annotations_test.go
@@ -0,0 +1,95 @@
+package v1alpha1
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestRefreshAnnotationValue(t *testing.T) {
+	t.Run("has refresh annotation", func(t *testing.T) {
+		result, ok := RefreshAnnotationValue(map[string]string{
+			AnnotationKeyRefresh: "foo",
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result)
+	})
+
+	t.Run("does not have refresh annotation", func(t *testing.T) {
+		result, ok := RefreshAnnotationValue(nil)
+		require.False(t, ok)
+		require.Empty(t, result)
+	})
+
+	t.Run("has refresh annotation with empty value", func(t *testing.T) {
+		result, ok := RefreshAnnotationValue(map[string]string{
+			AnnotationKeyRefresh: "",
+		})
+		require.True(t, ok)
+		require.Empty(t, result)
+	})
+}
+
+func TestReverifyAnnotationValue(t *testing.T) {
+	t.Run("has reverify annotation with valid JSON", func(t *testing.T) {
+		result, ok := ReverifyAnnotationValue(map[string]string{
+			AnnotationKeyReverify: `{"id":"foo"}`,
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result.ID)
+	})
+
+	t.Run("has reverify annotation with ID string", func(t *testing.T) {
+		result, ok := ReverifyAnnotationValue(map[string]string{
+			AnnotationKeyReverify: "foo",
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result.ID)
+	})
+
+	t.Run("does not have reverify annotation", func(t *testing.T) {
+		result, ok := ReverifyAnnotationValue(nil)
+		require.False(t, ok)
+		require.Nil(t, result)
+	})
+
+	t.Run("has reverify annotation with empty ID", func(t *testing.T) {
+		result, ok := ReverifyAnnotationValue(map[string]string{
+			AnnotationKeyAbort: "",
+		})
+		require.False(t, ok)
+		require.Nil(t, result)
+	})
+}
+
+func TestAbortAnnotationValue(t *testing.T) {
+	t.Run("has abort annotation with valid JSON", func(t *testing.T) {
+		result, ok := AbortAnnotationValue(map[string]string{
+			AnnotationKeyAbort: `{"id":"foo"}`,
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result.ID)
+	})
+
+	t.Run("has abort annotation with ID string", func(t *testing.T) {
+		result, ok := AbortAnnotationValue(map[string]string{
+			AnnotationKeyAbort: "foo",
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result.ID)
+	})
+
+	t.Run("does not have abort annotation", func(t *testing.T) {
+		result, ok := AbortAnnotationValue(nil)
+		require.False(t, ok)
+		require.Nil(t, result)
+	})
+
+	t.Run("has abort annotation with empty ID", func(t *testing.T) {
+		result, ok := AbortAnnotationValue(map[string]string{
+			AnnotationKeyAbort: "",
+		})
+		require.False(t, ok)
+		require.Nil(t, result)
+	})
+}
diff --git a/api/v1alpha1/stage_helpers_test.go b/api/v1alpha1/stage_helpers_test.go
index b8bf3e77..e7bc627d 100644
--- a/api/v1alpha1/stage_helpers_test.go
+++ b/api/v1alpha1/stage_helpers_test.go
@@ -12,6 +12,136 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
+func TestVerificationRequest_Equals(t *testing.T) {
+	tests := []struct {
+		name     string
+		r1       *VerificationRequest
+		r2       *VerificationRequest
+		expected bool
+	}{
+		{
+			name:     "both nil",
+			r1:       nil,
+			r2:       nil,
+			expected: true,
+		},
+		{
+			name:     "one nil",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: false},
+			r2:       nil,
+			expected: false,
+		},
+		{
+			name:     "other nil",
+			r1:       nil,
+			r2:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: false},
+			expected: false,
+		},
+		{
+			name:     "different IDs",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: false},
+			r2:       &VerificationRequest{ID: "other-id", Actor: "fake-actor", ControlPlane: false},
+			expected: false,
+		},
+		{
+			name:     "different actors",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: true},
+			r2:       &VerificationRequest{ID: "fake-id", Actor: "other-actor", ControlPlane: true},
+			expected: false,
+		},
+		{
+			name:     "different control plane flags",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: true},
+			r2:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: false},
+			expected: false,
+		},
+		{
+			name:     "equal",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: true},
+			r2:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: true},
+			expected: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			require.Equal(t, tt.r1.Equals(tt.r2), tt.expected)
+		})
+	}
+}
+
+func TestVerificationRequest_HasID(t *testing.T) {
+	t.Run("verification request is nil", func(t *testing.T) {
+		var r *VerificationRequest
+		require.False(t, r.HasID())
+	})
+
+	t.Run("verification request has empty ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "",
+		}
+		require.False(t, r.HasID())
+	})
+
+	t.Run("verification request has ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "foo",
+		}
+		require.True(t, r.HasID())
+	})
+}
+
+func TestVerificationRequest_ForID(t *testing.T) {
+	t.Run("verification request is nil", func(t *testing.T) {
+		var r *VerificationRequest
+		require.False(t, r.ForID("foo"))
+	})
+
+	t.Run("verification request has ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "foo",
+		}
+		require.True(t, r.ForID("foo"))
+		require.False(t, r.ForID("bar"))
+	})
+
+	t.Run("verification request has empty ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "",
+		}
+		require.False(t, r.ForID(""))
+		require.False(t, r.ForID("foo"))
+	})
+}
+
+func TestVerificationRequest_String(t *testing.T) {
+	t.Run("verification request is nil", func(t *testing.T) {
+		var r *VerificationRequest
+		require.Empty(t, r.String())
+	})
+
+	t.Run("verification request is empty", func(t *testing.T) {
+		r := &VerificationRequest{}
+		require.Empty(t, r.String())
+	})
+
+	t.Run("verification request has empty ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "",
+		}
+		require.Empty(t, r.String())
+	})
+
+	t.Run("verification request has data", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID:           "foo",
+			Actor:        "fake-actor",
+			ControlPlane: true,
+		}
+		require.Equal(t, `{"id":"foo","actor":"fake-actor","controlPlane":true}`, r.String())
+	})
+}
+
 func TestGetStage(t *testing.T) {
 	scheme := k8sruntime.NewScheme()
 	require.NoError(t, SchemeBuilder.AddToScheme(scheme))
@@ -164,7 +294,9 @@ func TestReverifyStageFreight(t *testing.T) {
 			Name:      "fake-stage",
 		})
 		require.NoError(t, err)
-		require.Equal(t, "fake-id", stage.Annotations[AnnotationKeyReverify])
+		require.Equal(t, (&VerificationRequest{
+			ID: "fake-id",
+		}).String(), stage.Annotations[AnnotationKeyReverify])
 	})
 }
 
@@ -302,6 +434,8 @@ func TestAbortStageFreightVerification(t *testing.T) {
 			Name:      "fake-stage",
 		})
 		require.NoError(t, err)
-		require.Equal(t, "fake-id", stage.Annotations[AnnotationKeyAbort])
+		require.Equal(t, (&VerificationRequest{
+			ID: "fake-id",
+		}).String(), stage.Annotations[AnnotationKeyAbort])
 	})
 }
diff --git a/api/v1alpha1/stage_types_test.go b/api/v1alpha1/stage_types_test.go
index 15fbfc2a..b528d5d9 100644
--- a/api/v1alpha1/stage_types_test.go
+++ b/api/v1alpha1/stage_types_test.go
@@ -86,6 +86,46 @@ func TestFreightReferenceStackUpdateOrPush(t *testing.T) {
 	}
 }
 
+func TestVerificationInfoStack_Current(t *testing.T) {
+	testCases := []struct {
+		name           string
+		stack          VerificationInfoStack
+		expectedResult *VerificationInfo
+	}{
+		{
+			name:           "stack is nil",
+			stack:          nil,
+			expectedResult: nil,
+		},
+		{
+			name:           "stack is empty",
+			stack:          VerificationInfoStack{},
+			expectedResult: nil,
+		},
+		{
+			name: "stack has one element",
+			stack: VerificationInfoStack{
+				{ID: "foo"},
+			},
+			expectedResult: &VerificationInfo{ID: "foo"},
+		},
+		{
+			name: "stack has multiple elements",
+			stack: VerificationInfoStack{
+				{ID: "foo"},
+				{ID: "bar"},
+				{ID: "baz"},
+			},
+			expectedResult: &VerificationInfo{ID: "foo"},
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			require.Equal(t, testCase.expectedResult, testCase.stack.Current())
+		})
+	}
+}
+
 func TestVerificationInfoStack_UpdateOrPush(t *testing.T) {
 	testCases := []struct {
 		name          string
diff --git a/internal/controller/stages/verification_test.go b/internal/controller/stages/verification_test.go
index 20ecd87f..4ff7bd4f 100644
--- a/internal/controller/stages/verification_test.go
+++ b/internal/controller/stages/verification_test.go
@@ -31,6 +31,11 @@ func TestStartVerification(t *testing.T) {
 			reconciler: &reconciler{
 				nowFn: fakeNow,
 			},
+			stage: &kargoapi.Stage{
+				Status: kargoapi.StageStatus{
+					CurrentFreight: &kargoapi.FreightReference{},
+				},
+			},
 			assertions: func(t *testing.T, vi *kargoapi.VerificationInfo, err error) {
 				require.NoError(t, err)
 				require.Contains(
@@ -117,6 +122,9 @@ func TestStartVerification(t *testing.T) {
 				Status: kargoapi.StageStatus{
 					CurrentFreight: &kargoapi.FreightReference{
 						Name: "fake-id",
+						VerificationHistory: []kargoapi.VerificationInfo{{
+							ID: "fake-id",
+						}},
 					},
 				},
 			},
@@ -809,6 +817,14 @@ func TestAbortVerification(t *testing.T) {
 				nowFn: fakeNow,
 			},
 			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-actor",
+						}).String(),
+					},
+				},
 				Status: kargoapi.StageStatus{
 					CurrentFreight: &kargoapi.FreightReference{
 						VerificationInfo: &kargoapi.VerificationInfo{
@@ -820,6 +836,7 @@ func TestAbortVerification(t *testing.T) {
 			assertions: func(t *testing.T, vi *kargoapi.VerificationInfo) {
 				require.NotNil(t, vi)
 				require.Equal(t, vi.ID, "fake-id")
+				require.Equal(t, vi.Actor, "fake-actor")
 				require.Contains(
 					t,
 					vi.Message,
@@ -867,6 +884,14 @@ func TestAbortVerification(t *testing.T) {
 		{
 			name: "success",
 			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-actor",
+						}).String(),
+					},
+				},
 				Status: kargoapi.StageStatus{
 					CurrentFreight: &kargoapi.FreightReference{
 						VerificationInfo: &kargoapi.VerificationInfo{
@@ -897,6 +922,7 @@ func TestAbortVerification(t *testing.T) {
 			assertions: func(t *testing.T, vi *kargoapi.VerificationInfo) {
 				require.NotNil(t, vi)
 				require.Equal(t, "fake-id", vi.ID)
+				require.Equal(t, "fake-actor", vi.Actor)
 				require.Equal(t, kargoapi.VerificationPhaseAborted, vi.Phase)
 				require.Equal(t, "Verification aborted by user", vi.Message)
 				require.Equal(t, &kargoapi.AnalysisRunReference{
@@ -1204,20 +1230,30 @@ func TestBuildAnalysisRun(t *testing.T) {
 			},
 		},
 		{
-			name:       "Set actor annotation only if the user triggers re-verification",
+			name:       "Does not set promotion name if user triggers re-verification",
 			reconciler: &reconciler{},
 			stage: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyReverify:      "fake-id",
-						kargoapi.AnnotationKeyReverifyActor: "fake-user",
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:           "fake-id",
+							Actor:        kargoapi.EventActorAdmin,
+							ControlPlane: true,
+						}).String(),
 					},
 				},
 				Spec: &kargoapi.StageSpec{
 					Verification: &kargoapi.Verification{},
 				},
 				Status: kargoapi.StageStatus{
-					CurrentFreight: &kargoapi.FreightReference{Name: "fake-id"},
+					CurrentFreight: &kargoapi.FreightReference{
+						Name: "fake-id",
+						VerificationHistory: []kargoapi.VerificationInfo{
+							{
+								ID: "fake-id",
+							},
+						},
+					},
 					LastPromotion: &kargoapi.PromotionInfo{
 						Name: "fake-id",
 						Status: &kargoapi.PromotionStatus{
@@ -1237,23 +1273,32 @@ func TestBuildAnalysisRun(t *testing.T) {
 				require.NoError(t, err)
 				require.NotNil(t, ar)
 				require.NotContains(t, ar.Labels, kargoapi.PromotionLabelKey)
-				require.Equal(t, "fake-user", ar.Annotations[kargoapi.AnnotationKeyReverifyActor])
 			},
 		},
 		{
-			name:       "Set promotion name only if the controlplane components trigger re-verification",
+			name:       "Set promotion name only if the control plane triggers re-verification",
 			reconciler: &reconciler{},
 			stage: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyReverify: "fake-id",
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:           "fake-id",
+							ControlPlane: true,
+						}).String(),
 					},
 				},
 				Spec: &kargoapi.StageSpec{
 					Verification: &kargoapi.Verification{},
 				},
 				Status: kargoapi.StageStatus{
-					CurrentFreight: &kargoapi.FreightReference{Name: "fake-id"},
+					CurrentFreight: &kargoapi.FreightReference{
+						Name: "fake-id",
+						VerificationHistory: []kargoapi.VerificationInfo{
+							{
+								ID: "fake-id",
+							},
+						},
+					},
 					LastPromotion: &kargoapi.PromotionInfo{
 						Name: "fake-id",
 						Status: &kargoapi.PromotionStatus{
diff --git a/internal/kargo/kargo_test.go b/internal/kargo/kargo_test.go
index c01d9ef9..7ef06e47 100644
--- a/internal/kargo/kargo_test.go
+++ b/internal/kargo/kargo_test.go
@@ -3,7 +3,6 @@ package kargo
 import (
 	"strings"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -75,63 +74,505 @@ func TestNewPromotion(t *testing.T) {
 	}
 }
 
-func TestIgnoreAnnotationRemovalUpdates(t *testing.T) {
-	testCases := []struct {
-		name     string
-		old      client.Object
-		new      client.Object
-		expected bool
+func TestRefreshRequested_Update(t *testing.T) {
+	tests := []struct {
+		name      string
+		oldObject client.Object
+		newObject client.Object
+		want      bool
 	}{
 		{
-			name:     "nil",
-			old:      nil,
-			new:      nil,
-			expected: true,
+			name:      "no old or new object",
+			oldObject: nil,
+			newObject: nil,
+			want:      false,
 		},
 		{
-			name: "annotation removed",
-			old: &kargoapi.Stage{
+			name:      "no old object",
+			oldObject: nil,
+			newObject: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyRefresh: time.Now().Format(time.RFC3339),
+						kargoapi.AnnotationKeyRefresh: "foo",
 					},
 				},
 			},
-			new: &kargoapi.Stage{
+			want: false,
+		},
+		{
+			name: "no new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name: "no refresh annotation",
+			oldObject: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{},
 				},
 			},
-			expected: false,
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"other": "annotation",
+					},
+				},
+			},
+			want: false,
 		},
 		{
-			name: "annotation set",
-			old: &kargoapi.Stage{
+			name: "refresh annotation set on new object",
+			oldObject: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{},
 				},
 			},
-			new: &kargoapi.Stage{
+			newObject: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyRefresh: time.Now().Format(time.RFC3339),
+						kargoapi.AnnotationKeyRefresh: "foo",
 					},
 				},
 			},
-			expected: true,
+			want: true,
+		},
+		{
+			name: "refresh annotation removed from new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "refresh annotation value changed",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "bar",
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "refresh annotation value equal",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			want: false,
 		},
 	}
-	for _, tc := range testCases {
-		tc := tc
-		t.Run(tc.name, func(t *testing.T) {
-			p := IgnoreAnnotationRemoval{
-				Annotations: []string{kargoapi.AnnotationKeyRefresh},
-			}
-			e := event.UpdateEvent{
-				ObjectOld: tc.old,
-				ObjectNew: tc.new,
-			}
-			require.Equal(t, tc.expected, p.Update(e))
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := RefreshRequested{}
+			require.Equal(t, tt.want, p.Update(event.UpdateEvent{
+				ObjectOld: tt.oldObject,
+				ObjectNew: tt.newObject,
+			}))
+		})
+	}
+}
+
+func TestReverifyRequested_Update(t *testing.T) {
+	tests := []struct {
+		name      string
+		oldObject client.Object
+		newObject client.Object
+		want      bool
+	}{
+		{
+			name:      "no old or new object",
+			oldObject: nil,
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name:      "no old object",
+			oldObject: nil,
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "foo",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "no new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "foo",
+					},
+				},
+			},
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name: "no reverify annotation",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"other": "annotation",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "reverify annotation set on new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "foo",
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "reverify annotation removed from new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "empty reverify annotation value",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "reverify annotation ID changed",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "bar",
+						}).String(),
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "reverify annotation actor changed with same ID",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "foo",
+							Actor: "fake-actor",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "foo",
+							Actor: "real-actor",
+						}).String(),
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "reverify annotation ID equal",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := ReverifyRequested{}
+			require.Equal(t, tt.want, p.Update(event.UpdateEvent{
+				ObjectOld: tt.oldObject,
+				ObjectNew: tt.newObject,
+			}))
+		})
+	}
+}
+
+func TestAbortRequested_Update(t *testing.T) {
+	tests := []struct {
+		name      string
+		oldObject client.Object
+		newObject client.Object
+		want      bool
+	}{
+		{
+			name:      "no old or new object",
+			oldObject: nil,
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name:      "no old object",
+			oldObject: nil,
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "foo",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "no new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "foo",
+					},
+				},
+			},
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name: "no abort annotation",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"other": "annotation",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "abort annotation set on new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "foo",
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "abort annotation removed from new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "empty abort annotation value",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "abort annotation ID changed",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "bar",
+						}).String(),
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "abort annotation actor changed with same ID",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "foo",
+							Actor: "fake-actor",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "foo",
+							Actor: "real-actor",
+						}).String(),
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "abort annotation ID equal",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := AbortRequested{}
+			require.Equal(t, tt.want, p.Update(event.UpdateEvent{
+				ObjectOld: tt.oldObject,
+				ObjectNew: tt.newObject,
+			}))
 		})
 	}
 }
diff --git a/internal/webhook/stage/webhook_test.go b/internal/webhook/stage/webhook_test.go
index 7528c146..5a01938e 100644
--- a/internal/webhook/stage/webhook_test.go
+++ b/internal/webhook/stage/webhook_test.go
@@ -118,7 +118,7 @@ func TestDefault(t *testing.T) {
 			},
 		},
 		{
-			name: "set reverify actor when request doesn't come from kargo controlplane",
+			name: "set reverify actor when request doesn't come from kargo control plane",
 			webhook: &webhook{
 				admissionRequestFromContextFn: admission.RequestFromContext,
 				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
@@ -147,15 +147,15 @@ func TestDefault(t *testing.T) {
 			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
 				require.NoError(t, err)
 				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
-				require.Equal(t, "fake-id", stage.Annotations[kargoapi.AnnotationKeyReverify])
-				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverifyActor)
-				require.Equal(
-					t,
-					kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID: "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
 						Username: "real-user",
 					}),
-					stage.Annotations[kargoapi.AnnotationKeyReverifyActor],
-				)
+					ControlPlane: false,
+				}, rr)
 			},
 		},
 		{
@@ -180,8 +180,10 @@ func TestDefault(t *testing.T) {
 			stage: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyReverify:      "fake-id",
-						kargoapi.AnnotationKeyReverifyActor: "fake-user",
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-user",
+						}).String(),
 					},
 				},
 				Spec: &kargoapi.StageSpec{},
@@ -189,18 +191,363 @@ func TestDefault(t *testing.T) {
 			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
 				require.NoError(t, err)
 				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
-				require.Equal(t, "fake-id", stage.Annotations[kargoapi.AnnotationKeyReverify])
-				require.Equal(
-					t,
-					kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID: "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
 						Username: "real-user",
 					}),
-					stage.Annotations[kargoapi.AnnotationKeyReverifyActor],
-				)
+					ControlPlane: false,
+				}, rr)
+			},
+		},
+		{
+			name: "do not overwrite reverify actor when request comes from control plane",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return true
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "control-plane-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: kargoapi.EventActorAdmin,
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:           "fake-id",
+					Actor:        kargoapi.EventActorAdmin,
+					ControlPlane: true,
+				}, rr)
+			},
+		},
+		{
+			name: "overwite reverify actor when it has changed for the same ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "illegitimate-user",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:    "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{Username: "real-user"}),
+				}, rr)
+			},
+		},
+		{
+			name: "overwrite reverify control plane flag when it has changed for the same ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:           "fake-id",
+							Actor:        "fake-user",
+							ControlPlane: true,
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:           "fake-id",
+					Actor:        kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{Username: "real-user"}),
+					ControlPlane: false,
+				}, rr)
+			},
+		},
+		{
+			name: "ignore empty reverify annotation",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "",
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyReverify]
+				require.True(t, ok)
+				require.Empty(t, v)
+			},
+		},
+		{
+			name: "ignore reverify annotation with empty ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyReverify]
+				require.True(t, ok)
+				require.Equal(t, (&kargoapi.VerificationRequest{
+					ID: "",
+				}).String(), v)
+			},
+		},
+		{
+			name: "ignore unchanged reverify annotation",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-user",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyReverify]
+				require.True(t, ok)
+				require.Equal(t, (&kargoapi.VerificationRequest{
+					ID:    "fake-id",
+					Actor: "fake-user",
+				}).String(), v)
+			},
+		},
+		{
+			name: "set abort actor when request doesn't come from kargo control plane",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "fake-id",
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID: "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
+						Username: "real-user",
+					}),
+					ControlPlane: false,
+				}, rr)
 			},
 		},
 		{
-			name: "always clear reverify actor annotation when reverify annotation removed",
+			name: "overwrite with admission request user info if abort actor annotation exists",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-user",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID: "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
+						Username: "real-user",
+					}),
+					ControlPlane: false,
+				}, rr)
+			},
+		},
+		{
+			name: "do not overwrite abort actor when request comes from control plane",
 			webhook: &webhook{
 				admissionRequestFromContextFn: admission.RequestFromContext,
 				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
@@ -210,12 +557,227 @@ func TestDefault(t *testing.T) {
 			req: admission.Request{
 				AdmissionRequest: admissionv1.AdmissionRequest{
 					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "control-plane-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: kargoapi.EventActorAdmin,
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:           "fake-id",
+					Actor:        kargoapi.EventActorAdmin,
+					ControlPlane: true,
+				}, rr)
+			},
+		},
+		{
+			name: "overwite abort actor when it has changed for the same ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "illegitimate-user",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:    "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{Username: "real-user"}),
+				}, rr)
+			},
+		},
+		{
+			name: "overwrite abort control plane flag when it has changed for the same ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:           "fake-id",
+							Actor:        "fake-user",
+							ControlPlane: true,
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:           "fake-id",
+					Actor:        kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{Username: "real-user"}),
+					ControlPlane: false,
+				}, rr)
+			},
+		},
+		{
+			name: "ignore empty abort annotation",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "",
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyAbort]
+				require.True(t, ok)
+				require.Empty(t, v)
+			},
+		},
+		{
+			name: "ignore abort annotation with empty ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyAbort]
+				require.True(t, ok)
+				require.Equal(t, (&kargoapi.VerificationRequest{
+					ID: "",
+				}).String(), v)
+			},
+		},
+		{
+			name: "ignore unchanged abort annotation",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
 					OldObject: runtime.RawExtension{
 						Object: &kargoapi.Stage{
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{
-									kargoapi.AnnotationKeyReverify:      "fake-id",
-									kargoapi.AnnotationKeyReverifyActor: "real-user",
+									kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
 								},
 							},
 						},
@@ -223,12 +785,24 @@ func TestDefault(t *testing.T) {
 				},
 			},
 			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-user",
+						}).String(),
+					},
+				},
 				Spec: &kargoapi.StageSpec{},
 			},
 			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
 				require.NoError(t, err)
-				require.NotContains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
-				require.NotContains(t, stage.Annotations, kargoapi.AnnotationKeyReverifyActor)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyAbort]
+				require.True(t, ok)
+				require.Equal(t, (&kargoapi.VerificationRequest{
+					ID:    "fake-id",
+					Actor: "fake-user",
+				}).String(), v)
 			},
 		},
 	}