You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#288802 added server-side rejection of an inert recovery delay: create (createRuleDataSchema refine), update (validateMergedRuleAttributes → INVALID_STATE_TRANSITION_CONFIG), and the manage_rule agent-builder tool now all reject state_transition.recovering_count > 0 / recovering_timeframe when recovery_strategy is none or unset.
The rule form was not updated to match, so the primary authoring surface can still build the payload the API now rejects. The result is a submit-time 400 surfaced as an error toast whose message points at state_transition.recovering_count — a path the form does not map to any control, so the user gets no indication that the Recovery delay control is the thing to change.
This is the remaining unmet acceptance criterion of #279941 (RecoveryDelayField hidden in the GUI whenever recovery delay would be inert), which that issue scoped as proceeding "regardless of the backend decision".
Reproduction
Reachable in a single create session. The flyout defaults are not what protect you — compose_discover_flyout.tsx defaults recoveryStrategy: 'no_breach' and stateTransitionRecoveryDelayMode: 'immediate', which is valid.
Rules → Create rule (ES|QL compose flyout), advance to the Outcome step.
Set Recovery to "No recovery" (composeDiscoverRecoveryType).
Set Recovery delay to "Recoveries", leaving the default count of 2 (DEFAULT_RECOVERING_COUNT, recovery_delay_field.tsx).
Order does not matter. handleRecoveryTypeChange (compose_discover_flyout.tsx) clears the recovery query from both the sandbox and committed RHF state when leaving the query strategy, but leaves stateTransition.recoveringCount / recoveringTimeframe untouched — so arriving at "No recovery" from "Default recovery" with a delay already set is equally broken.
Hiding the control is not sufficient on its own
Hiding RecoveryDelayField when recoveryStrategy === 'none' makes step 2→3 above worse rather than better:
Pick "Default recovery", set Recovery delay to "Recoveries 2", then switch to "No recovery".
The control disappears, but form state still holds recoveringCount: 2 with stateTransitionRecoveryDelayMode: 'recoveries'.
mapStateTransition still emits recovering_count: 2, so the save still returns 400 — now with the offending control invisible.
So the fix must clearrecoveringCount / recoveringTimeframe, in handleRecoveryTypeChange's non-query branch alongside the existing recovery-query clearing. Hiding or disabling the control is a good addition on top (a disabled control with help text explains why the delay is unavailable, which is closer to the intent of #279941), not a substitute.
Edit path
Stored rules with the inert combination open in the normal flyout rather than the YAML fallback: isNonRepresentableRule lists 'none' among the representable recovery strategies (is_non_representable.ts). mapRuleResponseToFormValues restores recoveringCount and deriveRecoveryDelayModeFromStateTransition derives 'recoveries', so saving an unrelated edit re-emits the value and returns 400.
The population of such rules is frozen as of #288802 (no write path can create a new one) and is believed to be negligible, so this is lower priority than the create path — but if the UI clears rather than hides, editing these rules also self-heals on next save.
Use the API's inert condition, not the one in #279941
Worth flagging for whoever picks this up, because implementing the condition as written in #279941 would leave a hole.
#279941 states the delay is inert exactly when recovery_strategy is null/'none' AND no_data_strategy !== 'recover', describing no_data_strategy: 'recover' as a carve-out where the delay is honoured. The engine does not behave that way, and committed unit tests assert the opposite:
count_timeframe_strategy.ts early-returns basicResult for a no_data event when no_data_strategy === 'recover', bypassing count/timeframe gating entirely.
count_timeframe_strategy.test.ts covers this with cases titled "transitions active → inactive immediately, ignoring recovery delay" and "transitions recovering → inactive immediately, ignoring recovery delay".
The UI guard must therefore key off recovery_strategy == null || recovery_strategy === 'none', regardless of no_data_strategy, matching isRecoveryDelayAllowed in rule_data_schema.ts. Reusing that exported predicate directly is the safest option, since it keeps client and server conditions from drifting.
Separately, #279941's inert-condition paragraph should be corrected (or the director changed to honour the delay for no_data_strategy: 'recover', if that carve-out was the intended design). That reconciliation is out of scope here.
Acceptance criteria
Selecting "No recovery" clears stateTransition.recoveringCount / recoveringTimeframe so the form cannot emit an inert delay
RecoveryDelayField is hidden or disabled while recovery_strategy is none/unset, with help text explaining why
Problem
#288802 added server-side rejection of an inert recovery delay: create (
createRuleDataSchemarefine), update (validateMergedRuleAttributes→INVALID_STATE_TRANSITION_CONFIG), and themanage_ruleagent-builder tool now all rejectstate_transition.recovering_count > 0/recovering_timeframewhenrecovery_strategyisnoneor unset.The rule form was not updated to match, so the primary authoring surface can still build the payload the API now rejects. The result is a submit-time
400surfaced as an error toast whose message points atstate_transition.recovering_count— a path the form does not map to any control, so the user gets no indication that the Recovery delay control is the thing to change.This is the remaining unmet acceptance criterion of #279941 (
RecoveryDelayFieldhidden in the GUI whenever recovery delay would be inert), which that issue scoped as proceeding "regardless of the backend decision".Reproduction
Reachable in a single create session. The flyout defaults are not what protect you —
compose_discover_flyout.tsxdefaultsrecoveryStrategy: 'no_breach'andstateTransitionRecoveryDelayMode: 'immediate', which is valid.composeDiscoverRecoveryType).2(DEFAULT_RECOVERING_COUNT,recovery_delay_field.tsx).composeFormToCreateRequestemits{ recovery_strategy: 'none', state_transition: { recovering_count: 2 } }→400.Order does not matter.
handleRecoveryTypeChange(compose_discover_flyout.tsx) clears the recovery query from both the sandbox and committed RHF state when leaving thequerystrategy, but leavesstateTransition.recoveringCount/recoveringTimeframeuntouched — so arriving at "No recovery" from "Default recovery" with a delay already set is equally broken.Hiding the control is not sufficient on its own
Hiding
RecoveryDelayFieldwhenrecoveryStrategy === 'none'makes step 2→3 above worse rather than better:recoveringCount: 2withstateTransitionRecoveryDelayMode: 'recoveries'.mapStateTransitionstill emitsrecovering_count: 2, so the save still returns400— now with the offending control invisible.So the fix must clear
recoveringCount/recoveringTimeframe, inhandleRecoveryTypeChange's non-querybranch alongside the existing recovery-query clearing. Hiding or disabling the control is a good addition on top (a disabled control with help text explains why the delay is unavailable, which is closer to the intent of #279941), not a substitute.Edit path
Stored rules with the inert combination open in the normal flyout rather than the YAML fallback:
isNonRepresentableRulelists'none'among the representable recovery strategies (is_non_representable.ts).mapRuleResponseToFormValuesrestoresrecoveringCountandderiveRecoveryDelayModeFromStateTransitionderives'recoveries', so saving an unrelated edit re-emits the value and returns400.The population of such rules is frozen as of #288802 (no write path can create a new one) and is believed to be negligible, so this is lower priority than the create path — but if the UI clears rather than hides, editing these rules also self-heals on next save.
Use the API's inert condition, not the one in #279941
Worth flagging for whoever picks this up, because implementing the condition as written in #279941 would leave a hole.
#279941 states the delay is inert exactly when
recovery_strategy is null/'none' AND no_data_strategy !== 'recover', describingno_data_strategy: 'recover'as a carve-out where the delay is honoured. The engine does not behave that way, and committed unit tests assert the opposite:count_timeframe_strategy.tsearly-returnsbasicResultfor ano_dataevent whenno_data_strategy === 'recover', bypassing count/timeframe gating entirely.count_timeframe_strategy.test.tscovers this with cases titled "transitions active → inactive immediately, ignoring recovery delay" and "transitions recovering → inactive immediately, ignoring recovery delay".rejects an inert recovery delay even when no_data_strategy is "recover") and removed a now-invalidrecovering_count: 1from the director Scout spec.The UI guard must therefore key off
recovery_strategy == null || recovery_strategy === 'none', regardless ofno_data_strategy, matchingisRecoveryDelayAllowedinrule_data_schema.ts. Reusing that exported predicate directly is the safest option, since it keeps client and server conditions from drifting.Separately, #279941's inert-condition paragraph should be corrected (or the director changed to honour the delay for
no_data_strategy: 'recover', if that carve-out was the intended design). That reconciliation is out of scope here.Acceptance criteria
stateTransition.recoveringCount/recoveringTimeframeso the form cannot emit an inert delayRecoveryDelayFieldis hidden or disabled whilerecovery_strategyisnone/unset, with help text explaining whyisRecoveryDelayAllowed(ideally by reusing it) rather than [Alerting v2] Recovery delay is configurable and persisted even when it has no effect on execution #279941's stated conditionrecovery_delay_field.test.tsx/compose_mappersfor the strategy-switch clearing, asserting the mapped request omits the delayReferences