diff --git a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/problem.rs b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/problem.rs index 756c4444bf..d97e15aa1c 100644 --- a/crates/tracedecay-automation-runtime/src/automation/effect_runtime/problem.rs +++ b/crates/tracedecay-automation-runtime/src/automation/effect_runtime/problem.rs @@ -4,8 +4,8 @@ use tracedecay_automation::backend::AgentTaskFailureClass; use tracedecay_contracts::retained_surfaces::{AutomationRunProblemV1, AutomationRunRequestV1}; use tracedecay_contracts::{ ApplicationExecutionFailureClassV1, ApplicationProblem, ApplicationProblemEnvelope, - ApplicationUnavailableClassV1, CancellationSignal, CancellationStage, LegalAction, - ProblemOwningLayer, RequestAdmission, RequestContext, RetryDirective, SafeDiagnostic, + ApplicationUnavailableClassV1, CancellationSignal, CancellationStage, ProblemOwningLayer, + RequestAdmission, RequestContext, SafeDiagnostic, }; use crate::automation::run_ledger::AutomationRunLedgerRecord; @@ -23,15 +23,13 @@ pub fn reset_required_problem( operation, context, request, - ApplicationProblem::ResetRequired { - diagnostic: SafeDiagnostic::new( + ApplicationProblem::reset_required( + SafeDiagnostic::new( "application.automation-run.reset-required", "The interrupted automation run has no reconcilable terminal; preserve its exact run identity and reset it explicitly before reuse.", ) .map_err(contract_error)?, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::Reset], - }, + ), ) } @@ -44,15 +42,13 @@ pub fn indeterminate_external_effect_problem( operation, context, request, - ApplicationProblem::ResetRequired { - diagnostic: SafeDiagnostic::new( + ApplicationProblem::reset_required( + SafeDiagnostic::new( "application.automation-run.external-effect-indeterminate", "An external automation effect may have committed before its terminal was published; the effect will not be replayed and its exact run identity requires explicit reconciliation or reset.", ) .map_err(contract_error)?, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::Reset], - }, + ), ) } @@ -116,15 +112,13 @@ pub fn runtime_problem( return Ok(problem); } if error.reset_required_context().is_some() { - return Ok(ApplicationProblem::ResetRequired { - diagnostic: SafeDiagnostic::new( + return Ok(ApplicationProblem::reset_required( + SafeDiagnostic::new( "application.automation-run.reset-required", "The admitted automation authority requires an explicit reset before reuse.", ) .map_err(contract_error)?, - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::Reset], - }); + )); } ApplicationProblem::execution_failed( ApplicationExecutionFailureClassV1::Permanent, diff --git a/crates/tracedecay-cli/src/work_cli.rs b/crates/tracedecay-cli/src/work_cli.rs index e1b7629216..da8f737d06 100644 --- a/crates/tracedecay-cli/src/work_cli.rs +++ b/crates/tracedecay-cli/src/work_cli.rs @@ -633,6 +633,7 @@ mod tests { diagnostic, retry, legal_actions, + .. } = problem else { panic!("work reset must remain a typed reset-required problem"); diff --git a/crates/tracedecay-cli/src/workflow_cli.rs b/crates/tracedecay-cli/src/workflow_cli.rs index 428a52a59f..593fc01607 100644 --- a/crates/tracedecay-cli/src/workflow_cli.rs +++ b/crates/tracedecay-cli/src/workflow_cli.rs @@ -310,6 +310,7 @@ mod reset_problem_tests { diagnostic, retry, legal_actions, + .. } = problem else { panic!("workflow reset must remain a typed reset-required problem"); diff --git a/crates/tracedecay-contracts/src/result/envelope.rs b/crates/tracedecay-contracts/src/result/envelope.rs index 0d21281ca4..91ba79eaba 100644 --- a/crates/tracedecay-contracts/src/result/envelope.rs +++ b/crates/tracedecay-contracts/src/result/envelope.rs @@ -273,6 +273,7 @@ impl<'de> Deserialize<'de> for ApplicationProblemRecord { wire.kind, ApplicationProblemKind::Stale | ApplicationProblemKind::Unavailable + | ApplicationProblemKind::ResetRequired | ApplicationProblemKind::Saturated ); if detail.is_some() && !detailed_kind { @@ -356,6 +357,7 @@ impl<'de> Deserialize<'de> for ApplicationProblemRecord { diagnostic, retry: wire.retry, legal_actions: legal_actions.clone(), + detail: detail.clone().map(Box::new), } } (ApplicationProblemKind::Saturated, Some(diagnostic), None) => { diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index 79748fd36a..0b0956509b 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -213,6 +213,7 @@ pub enum ApplicationProblem { diagnostic: SafeDiagnostic, retry: RetryDirective, legal_actions: Vec, + detail: Option>, }, Saturated { diagnostic: SafeDiagnostic, @@ -285,6 +286,8 @@ enum ApplicationProblemWire { diagnostic: SafeDiagnostic, retry: RetryDirective, legal_actions: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + detail: Option>, }, Saturated { diagnostic: SafeDiagnostic, @@ -392,10 +395,12 @@ impl From for ApplicationProblemWire { diagnostic, retry, legal_actions, + detail, } => Self::ResetRequired { diagnostic, retry, legal_actions, + detail, }, ApplicationProblem::Saturated { diagnostic, @@ -537,10 +542,12 @@ impl ApplicationProblem { diagnostic, retry, legal_actions, + detail, } => Self::ResetRequired { diagnostic, retry, legal_actions, + detail, }, ApplicationProblemWire::Saturated { diagnostic, @@ -885,6 +892,12 @@ impl ApplicationProblem { legal_actions: vec![LegalAction::Retry], detail: Some(Box::new(detail)), }, + ApplicationProblemDetailV1::ResetRequired { .. } => Self::ResetRequired { + diagnostic, + retry: RetryDirective::Never, + legal_actions: vec![LegalAction::Reset], + detail: Some(Box::new(detail)), + }, } } @@ -892,6 +905,7 @@ impl ApplicationProblem { match self { Self::Stale { detail, .. } | Self::Unavailable { detail, .. } + | Self::ResetRequired { detail, .. } | Self::Saturated { detail, .. } => detail.as_deref(), Self::InvalidRequest { .. } | Self::NotFoundOrNotAuthorized { .. } @@ -899,7 +913,6 @@ impl ApplicationProblem { | Self::PartialEffect { .. } | Self::Unsupported { .. } | Self::ExecutionFailed { .. } - | Self::ResetRequired { .. } | Self::Cancelled { .. } | Self::TimedOut { .. } => None, } @@ -966,6 +979,7 @@ impl ApplicationProblem { diagnostic, retry: RetryDirective::Never, legal_actions: vec![LegalAction::Reset], + detail: None, } } diff --git a/crates/tracedecay-contracts/src/result/problem/tests.rs b/crates/tracedecay-contracts/src/result/problem/tests.rs index a0d2938635..9480a069ae 100644 --- a/crates/tracedecay-contracts/src/result/problem/tests.rs +++ b/crates/tracedecay-contracts/src/result/problem/tests.rs @@ -269,6 +269,7 @@ fn direct_serialization_rejects_an_invalid_terminal() { .expect("fixture diagnostic is valid"), retry: RetryDirective::AfterDelay, legal_actions: vec![LegalAction::Retry], + detail: None, }; assert!(serde_json::to_value(invalid).is_err()); } diff --git a/crates/tracedecay-contracts/src/result/problem_detail.rs b/crates/tracedecay-contracts/src/result/problem_detail.rs index 699c402ff2..eb04ce1a30 100644 --- a/crates/tracedecay-contracts/src/result/problem_detail.rs +++ b/crates/tracedecay-contracts/src/result/problem_detail.rs @@ -27,6 +27,16 @@ pub enum ApplicationProblemDetailV1 { /// A writer lock stayed held by other writers past its admission /// deadline. LockDeadline { resource: String, deadline_ms: u64 }, + /// A persisted store whose shape this binary does not open. It is served + /// in this typed state until the operator runs `remedy`, which deletes + /// the old data; nothing is migrated or backed up. + ResetRequired { + authority: String, + found_version: Option, + required_version: Option, + reason: String, + remedy: String, + }, } impl ApplicationProblemDetailV1 { @@ -44,12 +54,41 @@ impl ApplicationProblemDetailV1 { } } + /// The typed detail of a persisted-shape refusal, reset by `remedy`. + pub fn from_reset_required(error: &TraceDecayError, remedy: impl Into) -> Option { + let (authority, found_version, required_version) = match error { + TraceDecayError::ResetRequired { authority, .. } => (authority.clone(), None, None), + TraceDecayError::ProfileResetRequired { + component, + found_version, + required_version, + } => ( + (*component).to_owned(), + *found_version, + Some(*required_version), + ), + _ => return None, + }; + let reason = match error { + TraceDecayError::ResetRequired { reason, .. } => reason.clone(), + _ => error.to_string(), + }; + Some(Self::ResetRequired { + authority, + found_version, + required_version, + reason, + remedy: remedy.into(), + }) + } + /// Stable diagnostic code of the problem this detail names. pub const fn code(&self) -> &'static str { match self { Self::Parked { .. } => "application.code-index.parked", Self::StaleRefreshFrontier { .. } => "application.retained.refresh-frontier-stale", Self::LockDeadline { .. } => "application.lock-deadline", + Self::ResetRequired { .. } => "application.reset-required", } } @@ -72,6 +111,13 @@ impl ApplicationProblemDetailV1 { "The {resource} stayed busy past its {deadline_ms}ms admission deadline; retry \ the operation." ), + // The remedy leads so a long reason is what the bound cuts. + Self::ResetRequired { + authority, + reason, + remedy, + .. + } => format!("The {authority} requires an explicit reset with `{remedy}`: {reason}"), }; let folded = tracedecay_domain::fold_control_characters(&text); tracedecay_domain::utf8_prefix_at_or_before(folded.trim(), MAX_RENDERED_MESSAGE_BYTES) @@ -107,6 +153,28 @@ impl ApplicationProblemDetailV1 { ("Lock resource", resource.clone()), ("Lock deadline", format!("{deadline_ms}ms")), ], + Self::ResetRequired { + authority, + found_version, + required_version, + reason, + remedy, + } => { + let mut fields = vec![("Reset authority", authority.clone())]; + if let Some(required_version) = required_version { + fields.push(( + "Found version", + found_version.map_or_else( + || "unversioned".to_owned(), + |version| version.to_string(), + ), + )); + fields.push(("Required version", required_version.to_string())); + } + fields.push(("Reset reason", reason.clone())); + fields.push(("Reset remedy", remedy.clone())); + fields + } } } } diff --git a/crates/tracedecay-contracts/src/retained_surfaces/sdk/results/automation/tests.rs b/crates/tracedecay-contracts/src/retained_surfaces/sdk/results/automation/tests.rs index 2902b83136..8beb9490e9 100644 --- a/crates/tracedecay-contracts/src/retained_surfaces/sdk/results/automation/tests.rs +++ b/crates/tracedecay-contracts/src/retained_surfaces/sdk/results/automation/tests.rs @@ -17,7 +17,7 @@ use crate::retained_surfaces::{ use crate::{ ApplicationExecutionFailureClassV1, ApplicationProblem, ApplicationProblemEnvelope, ApplicationUnavailableClassV1, CancellationStage, EffectReceipt, EffectTermination, - IdempotencyKey, LegalAction, RequestId, ResolvedScope, RetryDirective, SafeDiagnostic, + IdempotencyKey, RequestId, ResolvedScope, SafeDiagnostic, }; fn zero_terminal(status: &str) -> Value { @@ -693,15 +693,13 @@ fn non_partial_problem_rejects_committed_memory_receipts() { let operation = retained_surface_application_operation(RetainedSurfaceOperation::FactStoreCurate) .expect("automation operation"); - let problem = ApplicationProblem::ResetRequired { - diagnostic: SafeDiagnostic::new( + let problem = ApplicationProblem::reset_required( + SafeDiagnostic::new( "application.automation-run.reset-required", "The exact admitted run requires reconciliation before it can resume", ) .expect("diagnostic"), - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::Reset], - }; + ); let problem = ApplicationProblemEnvelope::new( operation.result_contract().clone(), request_id.clone(), diff --git a/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs b/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs index 61346e5f4a..83ec84d619 100644 --- a/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs +++ b/crates/tracedecay-daemon-control/src/service/update_restore_tests.rs @@ -270,7 +270,6 @@ fn maintenance_window_waits_out_the_lease_of_the_daemon_it_just_stopped() { #[cfg(target_os = "linux")] #[test] fn restore_check_accepts_a_reset_required_daemon_and_returns_its_pending_reset() { - let _env_lock = lock_user_data_dir_test_env(); let fixture = DrainingDaemonFixture::new(); let authority = super::tests::seed_socket_authority(&fixture.socket_path); let listener = UnixListener::bind(&fixture.socket_path).expect("bind restored daemon"); @@ -292,9 +291,12 @@ fn restore_check_accepts_a_reset_required_daemon_and_returns_its_pending_reset() authority.auth_token().to_owned(), ); - let (state, _, socket, protocol) = - super::installed_service_status_snapshot(&fixture.runner, super::tests::TEST_BUILD_VERSION) - .expect("restored service snapshot"); + let (state, _, socket, protocol) = super::installed_service_status_snapshot( + &fixture.profile, + &fixture.runner, + super::tests::TEST_BUILD_VERSION, + ) + .expect("restored service snapshot"); server.join().expect("join restored daemon"); assert!( diff --git a/crates/tracedecay-daemon-service/src/application_surface/problems.rs b/crates/tracedecay-daemon-service/src/application_surface/problems.rs index 9c3a3d1ea1..0dbb7ae783 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/problems.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/problems.rs @@ -3,8 +3,9 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use tracedecay_contracts::{ - ApplicationContractError, ApplicationProblem, ApplicationProblemEnvelope, LegalAction, - ProblemOwningLayer, RequestId, ResultContractRef, RetryDirective, SafeDiagnostic, + ApplicationContractError, ApplicationProblem, ApplicationProblemDetailV1, + ApplicationProblemEnvelope, LegalAction, ProblemOwningLayer, RequestId, ResultContractRef, + RetryDirective, SafeDiagnostic, }; use tracedecay_daemon_protocol::{ ApplicationSurfaceAdapterError, CatalogBindingResolver, DispatchError, @@ -118,27 +119,20 @@ pub(super) fn http_adapter_problem( /// The refusal settles before any project server exists, so the MCP boundary /// cannot route the call to its handler; the truthful answer for the named /// operation is the reset-required terminal under its own mounted MCP result -/// contract, naming `reset_command`, the exact command that performs the one -/// legal action, so the agent can relay it. Returns `None` for tools without -/// a mounted application binding. +/// contract, whose typed detail names the refused authority, its versions, +/// and the exact command that performs the one legal action, so the agent can +/// relay it. Returns `None` for tools without a mounted application binding. pub fn mcp_project_open_reset_refusal( tool_name: &str, request_id: RequestId, - authority: &str, - reason: &str, - reset_command: &str, + detail: ApplicationProblemDetailV1, ) -> Option { let operation = ApplicationSurfaceOperation::from_tool_name(tool_name)?; let catalog = application_surface_catalog_ref().ok()?; let resolver = CatalogBindingResolver::new(catalog); let binding = resolve_application_binding(&resolver, BindingSurface::Mcp, operation)?; let contract = ResultContractRef::from_schema(&binding.result_schema); - let problem = ApplicationProblem::reset_required(SafeDiagnostic { - code: "application.surface.reset_required".to_owned(), - message: format!( - "The {authority} requires an explicit reset: {reason}. Reset it with `{reset_command}`" - ), - }); + let problem = ApplicationProblem::from_detail(detail); ApplicationProblemEnvelope::new(contract, request_id, problem) .ok() .map(|envelope| envelope.with_owning_layer(ProblemOwningLayer::Runtime)) diff --git a/crates/tracedecay-mcp/src/handlers/multi_root.rs b/crates/tracedecay-mcp/src/handlers/multi_root.rs index fda21f0ce8..5a22f96ebb 100644 --- a/crates/tracedecay-mcp/src/handlers/multi_root.rs +++ b/crates/tracedecay-mcp/src/handlers/multi_root.rs @@ -387,6 +387,7 @@ mod tests { diagnostic, retry, legal_actions, + .. } = daemon_problem(DaemonInvocationProblem::ResetRequired) else { panic!("multi-root reset must remain reset-required"); diff --git a/crates/tracedecay/src/daemon/profile_retained.rs b/crates/tracedecay/src/daemon/profile_retained.rs index bfef1233aa..22450e59d3 100644 --- a/crates/tracedecay/src/daemon/profile_retained.rs +++ b/crates/tracedecay/src/daemon/profile_retained.rs @@ -97,14 +97,13 @@ pub(super) async fn invoke_profile_retained( /// A profile authority that could not be mounted keeps its reason code and /// retry verdict as a typed unavailable terminal. fn authority_problem(error: &TraceDecayError) -> ApplicationProblem { - if let Some((authority, reason)) = tracedecay_mcp::reset_required_context(error) { - return ApplicationProblem::reset_required(SafeDiagnostic { - code: "application.retained.profile-reset-required".to_owned(), - message: format!( - "The {authority} requires an explicit reset: {reason}. Reset it with `{}`", - tracedecay_mcp::reset_required_command(&authority, None) - ), - }); + if let Some((authority, _)) = tracedecay_mcp::reset_required_context(error) + && let Some(detail) = tracedecay_contracts::ApplicationProblemDetailV1::from_reset_required( + error, + tracedecay_mcp::reset_required_command(&authority, None), + ) + { + return ApplicationProblem::from_detail(detail); } let (code, retryable) = match error { TraceDecayError::ProjectRoute { diff --git a/crates/tracedecay/src/daemon/project_open_handshake.rs b/crates/tracedecay/src/daemon/project_open_handshake.rs index 58ce8c578c..bd0709c794 100644 --- a/crates/tracedecay/src/daemon/project_open_handshake.rs +++ b/crates/tracedecay/src/daemon/project_open_handshake.rs @@ -205,18 +205,17 @@ fn tool_call_open_refusal_response( if !matches!(classify_mcp_method(&request.method), McpMethod::ToolsCall) { return None; } - let (authority, reason) = tracedecay_mcp::reset_required_context(error)?; + let (authority, _) = tracedecay_mcp::reset_required_context(error)?; + let detail = tracedecay_contracts::ApplicationProblemDetailV1::from_reset_required( + error, + tracedecay_mcp::reset_required_command(&authority, None), + )?; let id = request.id.clone()?; let tool_name = request.params.as_ref()?.get("name")?.as_str()?; let request_id = tracedecay_contracts::request_identity::mcp_connection_request_id(&id, connection_scope)?; - let reset_command = tracedecay_mcp::reset_required_command(&authority, None); let envelope = tracedecay_daemon_service::application_surface::mcp_project_open_reset_refusal( - tool_name, - request_id, - &authority, - &reason, - &reset_command, + tool_name, request_id, detail, )?; let text = serde_json::to_string(&envelope).ok()?; let problem = serde_json::to_value(envelope.problem.as_ref()).ok()?; diff --git a/crates/tracedecay/src/daemon/tests/logging.rs b/crates/tracedecay/src/daemon/tests/logging.rs index 806964220e..eb88e73a4a 100644 --- a/crates/tracedecay/src/daemon/tests/logging.rs +++ b/crates/tracedecay/src/daemon/tests/logging.rs @@ -5,9 +5,8 @@ use tracedecay_runtime_core::logging::format_daemon_log_line; fn scheduler_application_problem_log_excludes_hostile_payload() { use tracedecay_contracts::retained_surfaces::AutomationRunProblemV1; use tracedecay_contracts::{ - ApplicationProblem, ApplicationProblemEnvelope, LegalAction, RequestId, ResolvedScope, - RetainedSurfaceOperation, RetryDirective, SafeDiagnostic, - retained_surface_application_operation, + ApplicationProblem, ApplicationProblemEnvelope, RequestId, ResolvedScope, + RetainedSurfaceOperation, SafeDiagnostic, retained_surface_application_operation, }; use tracedecay_domain::{ProjectId, RepositoryId, WorktreeId}; @@ -18,15 +17,13 @@ fn scheduler_application_problem_log_excludes_hostile_payload() { let envelope = ApplicationProblemEnvelope::new( operation.result_contract().clone(), request_id.clone(), - ApplicationProblem::ResetRequired { - diagnostic: SafeDiagnostic::new( + ApplicationProblem::reset_required( + SafeDiagnostic::new( "application.memory-automation-run.reset-required", format!("hostile automatic fact content api_key={SECRET}"), ) .unwrap(), - retry: RetryDirective::Never, - legal_actions: vec![LegalAction::Reset], - }, + ), ) .unwrap(); let scope = ResolvedScope::new( diff --git a/crates/tracedecay/tests/transport_acceptance_suite/typed_terminal_restart_acceptance/reset_required_serving.rs b/crates/tracedecay/tests/transport_acceptance_suite/typed_terminal_restart_acceptance/reset_required_serving.rs index e555b3f65d..e4bb4b618f 100644 --- a/crates/tracedecay/tests/transport_acceptance_suite/typed_terminal_restart_acceptance/reset_required_serving.rs +++ b/crates/tracedecay/tests/transport_acceptance_suite/typed_terminal_restart_acceptance/reset_required_serving.rs @@ -152,13 +152,23 @@ fn reset_required_profile_session_store_is_served_typed_until_its_named_reset() "remedy": "tracedecay wipe --all --yes", }]) ); - super::assert_reset_required( - &super::cli_problem_envelope( - &profile_session_read(&home_path, &project_path), - "profile session read over a reset-required store", - ), + let refused = super::cli_problem_envelope( + &profile_session_read(&home_path, &project_path), "profile session read over a reset-required store", ); + super::assert_reset_required(&refused, "profile session read over a reset-required store"); + assert_eq!( + refused["problem"]["detail"], + json!({ + "kind": "reset_required", + "authority": "git correlation", + "found_version": 5, + "required_version": 6, + "reason": "git correlation profile schema 5 is incompatible with required schema 6; \ + reset the profile", + "remedy": "tracedecay wipe --all --yes", + }) + ); wait_for_code_index_hit(&home_path, &project_path, "probe"); // `tracedecay wipe --all --yes` takes the profile offline by stopping the diff --git a/dashboard/codegen/schemas/dashboard-contracts.schema.json b/dashboard/codegen/schemas/dashboard-contracts.schema.json index 50dd95f5e2..585cccf38a 100644 --- a/dashboard/codegen/schemas/dashboard-contracts.schema.json +++ b/dashboard/codegen/schemas/dashboard-contracts.schema.json @@ -1418,6 +1418,48 @@ "deadline_ms" ], "type": "object" + }, + { + "additionalProperties": false, + "description": "A persisted store whose shape this binary does not open. It is served\nin this typed state until the operator runs `remedy`, which deletes\nthe old data; nothing is migrated or backed up.", + "properties": { + "authority": { + "type": "string" + }, + "found_version": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "kind": { + "const": "reset_required", + "type": "string" + }, + "reason": { + "type": "string" + }, + "remedy": { + "type": "string" + }, + "required_version": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "kind", + "authority", + "found_version", + "required_version", + "reason", + "remedy" + ], + "type": "object" } ] }, diff --git a/dashboard/src/contracts/generated.ts b/dashboard/src/contracts/generated.ts index 1606ddc0d2..895a350b9c 100644 --- a/dashboard/src/contracts/generated.ts +++ b/dashboard/src/contracts/generated.ts @@ -394,6 +394,13 @@ export const ApplicationProblemDetailV1Schema = z.discriminatedUnion("kind", [z. kind: z.literal("parked"), remedy: z.string(), retries_on_wake: z.boolean(), +}).strict(), z.object({ + authority: z.string(), + found_version: z.number().int().safe().nullable(), + kind: z.literal("reset_required"), + reason: z.string(), + remedy: z.string(), + required_version: z.number().int().safe().nullable(), }).strict(), z.object({ active: z.number().int().safe().min(0), committed: z.number().int().safe().min(0),