Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ impl FeedbackCycleRuntimeError {
pub fn lsp_failure_class(&self) -> &'static str {
match self {
Self::Contract(_) => "feedback-cycle-contract",
Self::Runtime(FeedbackRuntimeError::HandleStore(TraceDecayError::SyncLock {
Self::Runtime(FeedbackRuntimeError::HandleStore(TraceDecayError::LockDeadline {
..
})) => "feedback-cycle-handle-store-busy",
Self::Runtime(_) => "feedback-cycle-runtime",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1074,12 +1074,14 @@ async fn held_handle_store_lock_surfaces_the_typed_deadline_miss() {
error.source()
})
.find_map(|error| error.downcast_ref::<TraceDecayError>());
let expected = format!(
"response-handle writer lock at {} stayed contended past its admission deadline; retry the operation",
lock_path.display()
);
assert!(
matches!(cause, Some(TraceDecayError::SyncLock { message }) if *message == expected),
matches!(
cause,
Some(TraceDecayError::LockDeadline {
resource: "response-handle writer lock",
deadline_ms: 10_000,
})
),
"{error:?}"
);
assert_eq!(
Expand Down
1 change: 1 addition & 0 deletions crates/tracedecay-application/src/operation_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ impl OperationEventError {
)?,
retry: RetryDirective::AfterRevalidate,
legal_actions: vec![LegalAction::Refresh],
detail: None,
},
Self::InvalidFrontier => ApplicationProblem::conflict(
"operation_event.invalid_frontier",
Expand Down
30 changes: 15 additions & 15 deletions crates/tracedecay-application/src/primitives/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ use tracedecay_contracts::retrieval::{
};
use tracedecay_contracts::{
ApplicationContractError, ApplicationEnvelope, ApplicationOperation, ApplicationOutcome,
ApplicationProblem, ApplicationProblemEnvelope, ApplicationProblemKind, ApplicationResult,
AuthorityReceipt, CancellationContext, CancellationObservation, CancellationStage,
CapabilityGrantId, CapabilityGrantSnapshot, CoverageCompleteness, CoverageDomainState,
Deadline, DisclosureClass, EvidenceCoverage, EvidenceDomain, EvidencePacket, FreshnessState,
LegalAction, Omission, OmissionReason, OpaqueCursor, OperationBudgetUsage, OperationReceipt,
OperationTermination, PageCursor, PageRequest, PageState, PolicyDecisionRef, RequestAdmission,
RequestContext, RequestCostReceiptV1, RequestId, ResolvedScope, RetrievalEvidence,
RetryDirective, SafeDiagnostic, TemporalState,
ApplicationProblem, ApplicationProblemDetailV1, ApplicationProblemEnvelope,
ApplicationProblemKind, ApplicationResult, AuthorityReceipt, CancellationContext,
CancellationObservation, CancellationStage, CapabilityGrantId, CapabilityGrantSnapshot,
CoverageCompleteness, CoverageDomainState, Deadline, DisclosureClass, EvidenceCoverage,
EvidenceDomain, EvidencePacket, FreshnessState, LegalAction, Omission, OmissionReason,
OpaqueCursor, OperationBudgetUsage, OperationReceipt, OperationTermination, PageCursor,
PageRequest, PageState, PolicyDecisionRef, RequestAdmission, RequestContext,
RequestCostReceiptV1, RequestId, ResolvedScope, RetrievalEvidence, RetryDirective,
SafeDiagnostic, TemporalState,
};
use tracedecay_domain::text::forward_slash_path;
use tracedecay_domain::{CodeGenerationId, CommitId, ComponentVersion, UtcMicros};
Expand Down Expand Up @@ -472,8 +473,7 @@ impl OwnedPrimitiveRuntime {
impl OwnedPrimitiveRuntime {
/// A code-index read refused as retryable while the worktree is parked
/// would be retried forever: nothing converges until the operator acts.
/// The park's remedy and cause replace the generic refusal; the remedy
/// leads so a long cause is what the diagnostic bound cuts.
/// The park's typed cause and remedy replace the generic refusal.
async fn parked_refusal(&self, refusal: ApplicationProblemEnvelope) -> PrimitiveResult<Value> {
let Some(parked) = self
.convergence_park
Expand All @@ -482,14 +482,14 @@ impl OwnedPrimitiveRuntime {
else {
return Ok(Err(refusal));
};
let message = safe_problem_message(&format!(
"The code index for this worktree is parked; remedy: {}; cause: {}",
parked.remediation, parked.reason
));
Ok(Err(ApplicationProblemEnvelope::new(
refusal.contract,
refusal.request_id,
ApplicationProblem::code_index_parked(message),
ApplicationProblem::from_detail(ApplicationProblemDetailV1::Parked {
cause: parked.reason,
remedy: parked.remediation,
retries_on_wake: parked.retries_on_wake,
}),
)?))
}
}
Expand Down
29 changes: 15 additions & 14 deletions crates/tracedecay-contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,20 +334,21 @@ pub use project_registry::{
pub use remote::status::RemoteOperationalStatusReaderV1;
pub use result::{
APPLICATION_PROBLEM_REVISION, ApplicationEnvelope, ApplicationExecutionFailureClassV1,
ApplicationOutcome, ApplicationProblem, ApplicationProblemEnvelope, ApplicationProblemKind,
ApplicationProblemRecord, ApplicationResult, ApplicationUnavailableClassV1, AuthorityReceipt,
BudgetClass, CancellationObservation, CancellationStage, ContextMemoryAnalyticsV1,
CoverageCompleteness, CoverageDomainState, EffectId, EffectReceipt, EffectResult,
EffectTermination, EvidenceAuthority, EvidenceCoverage, EvidenceDomain, EvidenceIdentity,
EvidencePacket, EvidenceScore, EvidenceScoreKind, EvidenceScoreValue, FreshnessState,
IdempotencyKey, InvocationAnalyticsV1, LegalAction, Omission, OmissionReason, OpaqueCursor,
OperationBudgetUsage, OperationReceipt, OperationTermination, PageCursor, PageState,
PolicyDecisionRef, PrContextAnalyticsV1, PrContextStageTimingsV1, PreviewId, PreviewResult,
ProblemOwningLayer, ProblemTerminality, RUNTIME_MOUNTING_REASON_CODE, ReconciliationState,
RequestCostReceiptV1, ResultContractRef, ResumeToken, RetrievalEvidence, RetrieverContribution,
RetrieverContributionState, RetryDirective, RetryScope, SafeDiagnostic, ScoreId,
StorePointReadsV1, StreamEvent, StreamEventKind, StreamFrontier, StreamGap, StreamTermination,
StreamValidationError, TemporalState, validate_stream,
ApplicationOutcome, ApplicationProblem, ApplicationProblemDetailV1, ApplicationProblemEnvelope,
ApplicationProblemKind, ApplicationProblemRecord, ApplicationResult,
ApplicationUnavailableClassV1, AuthorityReceipt, BudgetClass, CancellationObservation,
CancellationStage, ContextMemoryAnalyticsV1, CoverageCompleteness, CoverageDomainState,
EffectId, EffectReceipt, EffectResult, EffectTermination, EvidenceAuthority, EvidenceCoverage,
EvidenceDomain, EvidenceIdentity, EvidencePacket, EvidenceScore, EvidenceScoreKind,
EvidenceScoreValue, FreshnessState, IdempotencyKey, InvocationAnalyticsV1, LegalAction,
Omission, OmissionReason, OpaqueCursor, OperationBudgetUsage, OperationReceipt,
OperationTermination, PageCursor, PageState, PolicyDecisionRef, PrContextAnalyticsV1,
PrContextStageTimingsV1, PreviewId, PreviewResult, ProblemOwningLayer, ProblemTerminality,
RUNTIME_MOUNTING_REASON_CODE, ReconciliationState, RequestCostReceiptV1, ResultContractRef,
ResumeToken, RetrievalEvidence, RetrieverContribution, RetrieverContributionState,
RetryDirective, RetryScope, SafeDiagnostic, ScoreId, StorePointReadsV1, StreamEvent,
StreamEventKind, StreamFrontier, StreamGap, StreamTermination, StreamValidationError,
TemporalState, validate_stream,
};
pub use retained_receipts::{
PreparedRetainedEffect, authority_receipt, effective_memory_deadline, evidence_outcome,
Expand Down
1 change: 1 addition & 0 deletions crates/tracedecay-contracts/src/remote/composition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ fn remote_unavailable(code: &str, message: &str) -> ApplicationProblem {
.expect("static remote problem diagnostic is valid"),
retry: RetryDirective::AfterRevalidate,
legal_actions: vec![LegalAction::Refresh, LegalAction::Reconcile],
detail: None,
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/tracedecay-contracts/src/remote/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ pub fn remote_protocol_problem(
)?,
retry: RetryDirective::AfterRevalidate,
legal_actions: vec![LegalAction::Refresh],
detail: None,
},
RemoteProtocolFailureV1::AuthorityReachable => ApplicationProblem::conflict(
"remote.authority_reachable",
Expand All @@ -571,6 +572,7 @@ pub fn remote_protocol_problem(
)?,
retry: RetryDirective::AfterDelay,
legal_actions: vec![LegalAction::Retry],
detail: None,
},
};
ApplicationProblemEnvelope::new(contract, request_id, problem)
Expand Down
1 change: 1 addition & 0 deletions crates/tracedecay-contracts/src/remote/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ fn invalid_status() -> ApplicationProblem {
.expect("static Remote operational status diagnostic is valid"),
retry: RetryDirective::AfterRevalidate,
legal_actions: vec![LegalAction::Refresh, LegalAction::Reconcile],
detail: None,
}
}

Expand Down
104 changes: 101 additions & 3 deletions crates/tracedecay-contracts/src/result/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ use crate::context::{RequestId, ResolvedScope};
use crate::error::ApplicationContractError;

use super::{
ApplicationExecutionFailureClassV1, ApplicationProblem, ApplicationProblemKind,
ApplicationUnavailableClassV1, CancellationStage, EffectReceipt, EffectResult,
EvidenceCoverage, EvidencePacket, LegalAction, PreviewResult, ProblemOwningLayer,
ApplicationExecutionFailureClassV1, ApplicationProblem, ApplicationProblemDetailV1,
ApplicationProblemKind, ApplicationUnavailableClassV1, CancellationStage, EffectReceipt,
EffectResult, EvidenceCoverage, EvidencePacket, LegalAction, PreviewResult, ProblemOwningLayer,
ProblemTerminality, RUNTIME_MOUNTING_REASON_CODE, RetryDirective, RetryScope, SafeDiagnostic,
};

Expand Down Expand Up @@ -196,6 +196,11 @@ pub struct ApplicationProblemRecord {
pub code: String,
pub message: String,
pub diagnostic: Option<SafeDiagnostic>,
/// The structured facts `message` is rendered from, when the problem has
/// any. Always serialized so a client never mistakes an omitted detail
/// for a problem without one.
#[schemars(with = "RequiredNullable<ApplicationProblemDetailV1>")]
pub detail: Option<ApplicationProblemDetailV1>,
/// A committed effect is present only for an admitted partial effect.
/// The nullable field is always serialized: omitting it would create a
/// compatibility/default path that could hide a missing receipt.
Expand Down Expand Up @@ -243,6 +248,7 @@ impl<'de> Deserialize<'de> for ApplicationProblemRecord {
code: String,
message: String,
diagnostic: Option<SafeDiagnostic>,
detail: RequiredNullable<ApplicationProblemDetailV1>,
committed_receipt: RequiredNullable<EffectReceipt>,
owning_layer: ProblemOwningLayer,
terminality: ProblemTerminality,
Expand All @@ -262,6 +268,18 @@ impl<'de> Deserialize<'de> for ApplicationProblemRecord {

let wire = Wire::deserialize(deserializer)?;
let legal_actions = wire.legal_actions;
let detail = wire.detail.0;
let detailed_kind = matches!(
wire.kind,
ApplicationProblemKind::Stale
| ApplicationProblemKind::Unavailable
| ApplicationProblemKind::Saturated
);
if detail.is_some() && !detailed_kind {
return Err(serde::de::Error::custom(
"application problem kind carries no detail",
));
}
let source = match (
wire.kind,
wire.diagnostic.clone(),
Expand Down Expand Up @@ -299,6 +317,7 @@ impl<'de> Deserialize<'de> for ApplicationProblemRecord {
diagnostic,
retry: wire.retry,
legal_actions: legal_actions.clone(),
detail: detail.clone().map(Box::new),
},
(ApplicationProblemKind::Unsupported, Some(diagnostic), None) => {
ApplicationProblem::Unsupported {
Expand All @@ -317,6 +336,7 @@ impl<'de> Deserialize<'de> for ApplicationProblemRecord {
diagnostic,
retry: wire.retry,
legal_actions: legal_actions.clone(),
detail: detail.clone().map(Box::new),
}
}
(ApplicationProblemKind::ExecutionFailed, Some(diagnostic), None) => {
Expand All @@ -343,6 +363,7 @@ impl<'de> Deserialize<'de> for ApplicationProblemRecord {
diagnostic,
retry: wire.retry,
legal_actions: legal_actions.clone(),
detail: detail.clone().map(Box::new),
}
}
(ApplicationProblemKind::Cancelled, None, None) => ApplicationProblem::Cancelled {
Expand Down Expand Up @@ -371,6 +392,7 @@ impl<'de> Deserialize<'de> for ApplicationProblemRecord {
code: wire.code,
message: wire.message,
diagnostic: wire.diagnostic,
detail,
committed_receipt: wire.committed_receipt.0,
owning_layer: wire.owning_layer,
terminality: wire.terminality,
Expand Down Expand Up @@ -492,6 +514,7 @@ impl ApplicationProblemRecord {
RetryDirective::AfterReconcile => Some(RetryScope::SameOperation),
};
let diagnostic = source.diagnostic().cloned();
let detail = source.detail().cloned();
let committed_receipt = source.committed_receipt().cloned();
let code = diagnostic
.as_ref()
Expand All @@ -503,6 +526,7 @@ impl ApplicationProblemRecord {
code,
message: source.safe_message().to_owned(),
diagnostic,
detail,
committed_receipt,
owning_layer: ProblemOwningLayer::Application,
terminality: source.terminality(),
Expand Down Expand Up @@ -560,6 +584,7 @@ impl ApplicationProblemRecord {
.unwrap_or_else(|| self.source.canonical_code())
|| self.message != self.source.safe_message()
|| self.diagnostic.as_ref() != self.source.diagnostic()
|| self.detail.as_ref() != self.source.detail()
|| self.committed_receipt.as_ref() != self.source.committed_receipt()
{
return Err(ApplicationContractError::Inconsistent {
Expand Down Expand Up @@ -1064,6 +1089,79 @@ mod tests {
);
}

#[test]
fn a_detailed_problem_carries_its_facts_beside_the_rendered_message() {
let envelope = ApplicationProblemEnvelope::new(
contract(),
RequestId::new("request.detail.fixture").expect("request"),
ApplicationProblem::from_detail(ApplicationProblemDetailV1::Parked {
cause: "source unreadable".to_owned(),
remedy: "restore the mode, then run `tracedecay sync`".to_owned(),
retries_on_wake: false,
}),
)
.expect("parked envelope");
let wire = serde_json::to_value(&envelope).expect("envelope serializes");
assert_eq!(
wire["problem"]["detail"],
serde_json::json!({
"kind": "parked",
"cause": "source unreadable",
"remedy": "restore the mode, then run `tracedecay sync`",
"retries_on_wake": false,
})
);
assert_eq!(
(
&wire["problem"]["code"],
&wire["problem"]["message"],
&wire["problem"]["retry"],
&wire["problem"]["legal_actions"],
),
(
&serde_json::json!("application.code-index.parked"),
&serde_json::json!(
"The code index for this worktree is parked; remedy: restore the mode, then \
run `tracedecay sync`; cause: source unreadable"
),
&serde_json::json!("never"),
&serde_json::json!(["reconcile"]),
)
);
assert_eq!(
serde_json::from_value::<ApplicationProblemEnvelope>(wire.clone())
.expect("detailed envelope decodes"),
envelope
);

let mut restated = wire.clone();
restated["problem"]["detail"]["cause"] = serde_json::json!("another cause");
assert!(
serde_json::from_value::<ApplicationProblemEnvelope>(restated).is_err(),
"a message that no longer renders its detail must be refused"
);
let mut omitted = wire.clone();
omitted["problem"]
.as_object_mut()
.expect("problem object")
.remove("detail");
assert!(serde_json::from_value::<ApplicationProblemEnvelope>(omitted).is_err());

let plain = ApplicationProblemEnvelope::new(
contract(),
RequestId::new("request.detail.plain").expect("request"),
ApplicationProblem::invalid_request("result.invalid", "The request is invalid."),
)
.expect("plain envelope");
let mut grafted = serde_json::to_value(&plain).expect("plain envelope serializes");
assert_eq!(grafted["problem"]["detail"], Value::Null);
grafted["problem"]["detail"] = wire["problem"]["detail"].clone();
assert!(
serde_json::from_value::<ApplicationProblemEnvelope>(grafted).is_err(),
"a kind that carries no detail must not accept one"
);
}

fn retry_directed_record(code: &str, delay_millis: u64) -> ApplicationProblemRecord {
let envelope = ApplicationProblemEnvelope::new(
ResultContractRef::new(
Expand Down
2 changes: 2 additions & 0 deletions crates/tracedecay-contracts/src/result/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod analytics;
mod envelope;
mod evidence;
mod problem;
mod problem_detail;
mod receipt;
mod stream;

Expand All @@ -25,6 +26,7 @@ pub use problem::{
ApplicationUnavailableClassV1, LegalAction, ProblemOwningLayer, ProblemTerminality,
RUNTIME_MOUNTING_REASON_CODE, RetryDirective, RetryScope, SafeDiagnostic,
};
pub use problem_detail::ApplicationProblemDetailV1;
pub use receipt::{
CancellationObservation, CancellationStage, EffectId, EffectReceipt, EffectResult,
EffectTermination, IdempotencyKey, OperationBudgetUsage, OperationReceipt,
Expand Down
Loading
Loading