diff --git a/crates/tracedecay-api/src/http.rs b/crates/tracedecay-api/src/http.rs index 77458de5cc..72384a5fc0 100644 --- a/crates/tracedecay-api/src/http.rs +++ b/crates/tracedecay-api/src/http.rs @@ -421,7 +421,8 @@ pub(crate) fn invalid_request_problem( ) } -pub(crate) fn adapter_problem( +/// A refusal no binding owns, under the adapter-owned problem contract. +pub fn adapter_problem( request_id: RequestId, problem: ApplicationProblem, ) -> Result { diff --git a/crates/tracedecay-api/src/lib.rs b/crates/tracedecay-api/src/lib.rs index c8b20c1c9b..9fb5a4d693 100644 --- a/crates/tracedecay-api/src/lib.rs +++ b/crates/tracedecay-api/src/lib.rs @@ -47,9 +47,9 @@ pub use handoff::{ }; pub use http::{ HttpApplicationControls, HttpApplicationInvocationFuture, HttpApplicationOwnerKind, - HttpApplicationOwners, HttpApplicationRequest, HttpRouteDocumentV1, adapter_problem_response, - application_problem_response, application_problem_status, application_router, - configuration_application_router, feedback_application_router, + HttpApplicationOwners, HttpApplicationRequest, HttpRouteDocumentV1, adapter_problem, + adapter_problem_response, application_problem_response, application_problem_status, + application_router, configuration_application_router, feedback_application_router, http_application_full_route_path, http_application_owner_kind, http_application_route_path, http_route_documents, is_http_application_operation_exposed, }; diff --git a/crates/tracedecay-cli/src/tool_command.rs b/crates/tracedecay-cli/src/tool_command.rs index d32992c987..a704f3c624 100644 --- a/crates/tracedecay-cli/src/tool_command.rs +++ b/crates/tracedecay-cli/src/tool_command.rs @@ -69,7 +69,7 @@ use tracedecay_daemon_protocol::{ use tracedecay_daemon_service::application_surface::observe_surface_argument_rejection; use tracedecay_domain::UtcMicros; use tracedecay_domain::errors::{Result, TraceDecayError}; -use tracedecay_mcp::tool_errors::{project_route_problem, tool_result_problem}; +use tracedecay_mcp::tool_errors::{mark_semantic_tool_error, tool_result_problem}; use tracedecay_mcp::tools::binding::tool_dispatches_registered_project_reader; use tracedecay_mcp::tools::response_trailers::{ CODE_GRAPH_FRESHNESS_TRAILER_PREFIX, REQUEST_COST_TRAILER_PREFIX, @@ -79,7 +79,7 @@ use tracedecay_mcp::{ RESERVED_FLAGS_FOOTER, ToolDefinition, get_tool_definitions, render_tool_cli_help, short_tool_name, }; -use tracedecay_tool_catalog::ApplicationSurfaceOperation; +use tracedecay_tool_catalog::{ApplicationSurfaceOperation, BindingSurface}; use crate::cli::dispatch::resolve_cli_application_surface; @@ -149,13 +149,38 @@ pub(crate) async fn run( let json_requested = args.iter().any(|arg| arg == "--json"); let tool_name = name.as_deref().map(canonical_tool_name); let result = run_inner(profile, project, name, args).await; - if json_requested - && let (Err(error), Some(tool_name)) = (&result, tool_name.as_deref()) - && let Some(problem) = project_route_problem(tool_name, error) - { - println!("{}", serde_json::json!({ "problem": problem })); + match (&result, tool_name.as_deref()) { + (Err(error), Some(tool_name)) if json_requested => { + print_settled_route_refusal(tool_name, error).map_err(|print_error| { + TraceDecayError::Config { + message: format!( + "{error}; the --json refusal could not be rendered: {print_error}" + ), + } + })?; + result + } + _ => result, + } +} + +/// A `--json` call refused before any owner answered it prints the same +/// tool-result refusal an answered call does. +fn print_settled_route_refusal(tool_name: &str, error: &TraceDecayError) -> Result<()> { + let request_id = + mint_global_request_id(GlobalRequestSurface::Cli).map_err(|_| TraceDecayError::Config { + message: "could not allocate a refusal request id".to_owned(), + })?; + if let Some(rendered) = tracedecay::mcp::tools::render_settled_route_refusal( + BindingSurface::Cli, + tool_name, + request_id, + error, + &serde_json::json!({ "format": "json" }), + ) { + print_tool_output(&rendered?.value, true); } - result + Ok(()) } fn run_inner( @@ -989,11 +1014,15 @@ fn print_cli_application_surface( &result, )?; account_tool_result(project, &mut rendered); - if raw_json { - print!("{}", crate::cli::output::json::json_line(&result.result)?); - print_beside_result_blocks(&rendered.value); - } else { - print_tool_output(&rendered.value, false); + mark_semantic_tool_error(&mut rendered); + match (&result.result, raw_json) { + // A refusal prints as the tool result every route answers. + (Err(_), true) => print_tool_output(&rendered.value, true), + (Ok(_), true) => { + print!("{}", crate::cli::output::json::json_line(&result.result)?); + print_beside_result_blocks(&rendered.value); + } + (_, false) => print_tool_output(&rendered.value, false), } if application_problem.is_some() { std::io::stdout().flush()?; diff --git a/crates/tracedecay-cli/tests/core_cli_suite/tool_cursor_test.rs b/crates/tracedecay-cli/tests/core_cli_suite/tool_cursor_test.rs index 7b50ad9256..37f8e6fc25 100644 --- a/crates/tracedecay-cli/tests/core_cli_suite/tool_cursor_test.rs +++ b/crates/tracedecay-cli/tests/core_cli_suite/tool_cursor_test.rs @@ -627,3 +627,127 @@ fn search_format_json_prints_the_whole_typed_result() { stdout.len() ); } + +/// Where a `tool --json` stdout document carries its refusal: its members, +/// `isError`, and the members of its structured content. +fn refusal_placement(stdout: &str) -> Value { + let printed: Value = serde_json::from_str(stdout) + .unwrap_or_else(|error| panic!("refusal printed non-JSON ({error}):\n{stdout}")); + let members = |value: &Value| { + value + .as_object() + .map(|object| object.keys().cloned().collect::>()) + }; + json!({ + "members": members(&printed), + "is_error": printed["isError"], + "structured_members": members(&printed["structuredContent"]), + }) +} + +fn problem_of(stdout: &str) -> Value { + serde_json::from_str::(stdout).unwrap()["structuredContent"]["problem"].clone() +} + +/// A compatibility tool's refusal, a typed application tool's refusal, and a +/// refusal no daemon was there to answer all print one `--json` shape: the +/// typed problem record at `structuredContent.problem`, beside `isError`. +#[test] +fn every_json_refusal_prints_one_typed_problem_placement() { + let home = TempDir::new().unwrap(); + let project = TempDir::new().unwrap(); + let home = canonical_existing_path(home.path()); + let project = canonical_existing_path(project.path()); + committed_git_project(&project, &hub_source()); + initialize_tracedecay_cli_project(&home, &project); + hub_node_id(&home, &project); + + let search = run_tool( + &home, + &project, + "tracedecay_search", + &json!({"query": "hub", "limit": 2, "cursor": "not-a-cursor", "format": "json"}), + ); + let callers = run_tool( + &home, + &project, + "tracedecay_callers", + &json!({"node_id": "symbol.no-such-node"}), + ); + let socket = home.join("no-daemon.sock"); + let unreachable = tracedecay_command_with_home(&home) + .current_dir(&project) + .env("TRACEDECAY_DAEMON_SOCKET", &socket) + .args(["tool", "tracedecay_status", "--json"]) + .output() + .expect("tracedecay tool should run"); + stop_managed_daemon(&home); + let unreachable_stdout = String::from_utf8_lossy(&unreachable.stdout).into_owned(); + + let expected = json!({ + "members": ["content", "isError", "structuredContent"], + "is_error": true, + "structured_members": ["problem"], + }); + assert_eq!( + [ + refusal_placement(&search.stdout), + refusal_placement(&callers.stdout), + refusal_placement(&unreachable_stdout), + ], + [expected.clone(), expected.clone(), expected], + "search:\n{}\ncallers:\n{}\nunreachable:\n{unreachable_stdout}", + search.stdout, + callers.stdout, + ); + assert_eq!( + (search.success, callers.success, unreachable.status.code()), + (false, false, Some(69)) + ); + + let search = problem_of(&search.stdout); + let callers = problem_of(&callers.stdout); + let unreachable = problem_of(&unreachable_stdout); + assert_eq!( + [ + (&search["kind"], &search["code"], &search["retry"]), + (&callers["kind"], &callers["code"], &callers["retry"]), + ( + &unreachable["kind"], + &unreachable["diagnostic"]["code"], + &unreachable["retry"] + ), + ], + [ + ( + &json!("invalid_request"), + &json!("cursor.invalid"), + &json!("never") + ), + ( + &json!("not_found_or_not_authorized"), + &json!("not_found_or_not_authorized"), + &json!("never"), + ), + ( + &json!("unavailable"), + &json!("daemon.unreachable"), + &json!("after_delay") + ), + ], + "search {search}\ncallers {callers}\nunreachable {unreachable}" + ); + assert_eq!( + (&unreachable["detail"], &unreachable["legal_actions"]), + ( + &json!({ + "kind": "daemon_unreachable", + "socket": socket.display().to_string(), + "named_by": "TRACEDECAY_DAEMON_SOCKET", + "service_unit": {"state": "not_installed"}, + }), + &json!(["retry"]), + ), + "{unreachable}" + ); +} diff --git a/crates/tracedecay-cli/tests/core_cli_suite/tool_daemon_test.rs b/crates/tracedecay-cli/tests/core_cli_suite/tool_daemon_test.rs index 42a4ec0528..37acfb730f 100644 --- a/crates/tracedecay-cli/tests/core_cli_suite/tool_daemon_test.rs +++ b/crates/tracedecay-cli/tests/core_cli_suite/tool_daemon_test.rs @@ -1880,8 +1880,14 @@ fn configuration_tool_cli_persists_effects_and_fails_on_stale_cas() { ); let stale_payload: Value = serde_json::from_slice(&stale.stdout).expect("stale write problem JSON"); - assert_eq!(stale_payload["problem"]["kind"], "conflict"); - assert_eq!(stale_payload["problem"]["code"], "configuration.conflict"); + assert_eq!( + stale_payload["structuredContent"]["problem"]["kind"], + "conflict" + ); + assert_eq!( + stale_payload["structuredContent"]["problem"]["code"], + "configuration.conflict" + ); } #[test] @@ -3634,7 +3640,10 @@ fn user_settings_resolve_from_the_profile_outside_any_project() { let (code, refused) = configuration_get_from(&home, &outside, "index.max_file_size.v1"); assert_eq!(code, Some(1), "{refused}"); assert_eq!( - (&refused["problem"]["kind"], &refused["problem"]["code"]), + ( + &refused["structuredContent"]["problem"]["kind"], + &refused["structuredContent"]["problem"]["code"] + ), (&json!("invalid_request"), &json!("project_required")), "{refused}" ); diff --git a/crates/tracedecay-cli/tests/core_cli_suite/tool_surface_transport_test.rs b/crates/tracedecay-cli/tests/core_cli_suite/tool_surface_transport_test.rs index de01a3308a..854fceab14 100644 --- a/crates/tracedecay-cli/tests/core_cli_suite/tool_surface_transport_test.rs +++ b/crates/tracedecay-cli/tests/core_cli_suite/tool_surface_transport_test.rs @@ -87,7 +87,7 @@ impl SurfaceOutcome { fn problem_code(&self) -> Option { self.payload() - .get("problem") + .pointer("/structuredContent/problem") .and_then(|problem| problem.get("code")) .and_then(Value::as_str) .map(str::to_owned) @@ -138,19 +138,26 @@ fn tool_argument_errors_are_typed_invalid_requests() { "fact_store_list", &["--limit", "abc", "--json"], ); + let printed = outcome.payload(); + let problem = &printed["structuredContent"]["problem"]; assert_eq!( - outcome.payload(), - serde_json::json!({ - "problem": { - "tool": "tracedecay_fact_store_list", - "code": "tool_arguments_invalid", - "reason_code": "tool_arguments_invalid", - "kind": "invalid_request", - "retryable": false, - "detail": detail, - } - }), - "stderr:\n{}", + ( + &printed["isError"], + &problem["kind"], + &problem["code"], + &problem["message"], + &problem["retry"], + &problem["legal_actions"], + ), + ( + &serde_json::json!(true), + &serde_json::json!("invalid_request"), + &serde_json::json!("tool_arguments_invalid"), + &serde_json::json!(detail), + &serde_json::json!("never"), + &serde_json::json!([]), + ), + "stderr:\n{}\n{printed}", outcome.stderr ); assert_eq!( @@ -553,7 +560,7 @@ fn tool_diagnostics_names_the_install_command_without_a_compiler() { outcome.stdout, outcome.stderr ); - let problem = &outcome.payload()["problem"]; + let problem = &outcome.payload()["structuredContent"]["problem"]; assert_eq!( problem["legal_actions"], serde_json::json!(["refresh"]), @@ -581,7 +588,7 @@ fn tool_diagnostics_json_carries_the_unowned_scope_detail() { r#"{"scope":"file","path":"src/lib.rs"}"#, ); assert!(!outcome.success, "stdout:\n{}", outcome.stdout); - let problem = &outcome.payload()["problem"]; + let problem = &outcome.payload()["structuredContent"]["problem"]; assert_eq!( (&problem["code"], &problem["detail"]), ( @@ -639,7 +646,7 @@ fn tool_diagnostics_json_carries_the_pending_producer_detail() { } }; assert_eq!( - pending["problem"]["detail"], + pending["structuredContent"]["problem"]["detail"], serde_json::json!({ "kind": "diagnostics_pending", "producer": "node_modules/.bin/tsc", @@ -706,7 +713,7 @@ fn tool_diagnostics_names_pnpm_install_for_an_uninstalled_monorepo() { outcome.stdout, outcome.stderr ); - let problem = &outcome.payload()["problem"]; + let problem = &outcome.payload()["structuredContent"]["problem"]; assert_eq!( problem["legal_actions"], serde_json::json!(["refresh"]), @@ -872,10 +879,10 @@ fn work_and_workflow_tools_answer_through_their_typed_owner() { ); } -/// A refusal raised before dispatch answers `--json` with the same typed -/// problem record MCP puts in the JSON-RPC error `data`, so a shell caller -/// branches on `code` instead of parsing prose, and the refused edit writes -/// nothing. +/// A refusal raised before dispatch answers `--json` with the typed problem +/// record at `structuredContent.problem`, as every refusal does, so a shell +/// caller branches on `code` instead of parsing prose, and the refused edit +/// writes nothing. #[test] fn tool_json_reports_argument_refusals_as_typed_problems() { let (_home, _project, home_path, project_path) = surface_fixture(); @@ -910,19 +917,24 @@ fn tool_json_reports_argument_refusals_as_typed_problems() { "`tracedecay tool {tool}` must fail\nstdout:\n{}\nstderr:\n{}", outcome.stdout, outcome.stderr ); + let printed = outcome.payload(); + let problem = &printed["structuredContent"]["problem"]; assert_eq!( - outcome.payload(), - serde_json::json!({ - "problem": { - "tool": format!("tracedecay_{tool}"), - "code": "application_surface_invalid_request", - "reason_code": "application_surface_invalid_request", - "kind": "invalid_request", - "retryable": false, - "detail": detail, - } - }), - "stderr:\n{}", + ( + &printed["isError"], + &problem["kind"], + &problem["code"], + &problem["message"], + &problem["retry"], + ), + ( + &serde_json::json!(true), + &serde_json::json!("invalid_request"), + &serde_json::json!("application_surface_invalid_request"), + &serde_json::json!(detail), + &serde_json::json!("never"), + ), + "stderr:\n{}\n{printed}", outcome.stderr ); assert_eq!( @@ -947,7 +959,7 @@ fn tool_json_reports_argument_refusals_as_typed_problems() { "an unknown tool must fail: {}", unknown.stderr ); - let problem = &unknown.payload()["problem"]; + let problem = &unknown.payload()["structuredContent"]["problem"]; assert_eq!( (&problem["code"], &problem["kind"], &problem["retryable"]), ( diff --git a/crates/tracedecay-contracts/src/lib.rs b/crates/tracedecay-contracts/src/lib.rs index b514278a3d..7294288a8a 100644 --- a/crates/tracedecay-contracts/src/lib.rs +++ b/crates/tracedecay-contracts/src/lib.rs @@ -339,17 +339,17 @@ pub use result::{ ApplicationProblemKind, ApplicationProblemRecord, ApplicationResult, ApplicationUnavailableClassV1, AuthorityReceipt, BudgetClass, CancellationObservation, CancellationStage, ContextMemoryAnalyticsV1, CoverageCompleteness, CoverageDomainState, - DiagnosticsSearchedTsconfigV1, 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, + DaemonServiceUnitObservationV1, DiagnosticsSearchedTsconfigV1, 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, diff --git a/crates/tracedecay-contracts/src/result/mod.rs b/crates/tracedecay-contracts/src/result/mod.rs index ab58d77dda..bd097fc5b4 100644 --- a/crates/tracedecay-contracts/src/result/mod.rs +++ b/crates/tracedecay-contracts/src/result/mod.rs @@ -26,7 +26,9 @@ pub use problem::{ ApplicationUnavailableClassV1, LegalAction, ProblemOwningLayer, ProblemTerminality, RUNTIME_MOUNTING_REASON_CODE, RetryDirective, RetryScope, SafeDiagnostic, }; -pub use problem_detail::{ApplicationProblemDetailV1, DiagnosticsSearchedTsconfigV1}; +pub use problem_detail::{ + ApplicationProblemDetailV1, DaemonServiceUnitObservationV1, DiagnosticsSearchedTsconfigV1, +}; pub use receipt::{ CancellationObservation, CancellationStage, EffectId, EffectReceipt, EffectResult, EffectTermination, IdempotencyKey, OperationBudgetUsage, OperationReceipt, diff --git a/crates/tracedecay-contracts/src/result/problem.rs b/crates/tracedecay-contracts/src/result/problem.rs index 598e7526b3..f3ec982dfd 100644 --- a/crates/tracedecay-contracts/src/result/problem.rs +++ b/crates/tracedecay-contracts/src/result/problem.rs @@ -898,7 +898,8 @@ impl ApplicationProblem { /// the committed value. A lock /// deadline is capacity: the same request may succeed after a delay. A /// diagnostics scope no compiler owns is routed to publishing the - /// project's own check; a pending producer answers after a delay. + /// project's own check; a pending producer, and a daemon that may be + /// restarting, answer after a delay. pub fn from_detail(detail: ApplicationProblemDetailV1) -> Self { let diagnostic = SafeDiagnostic { code: detail.code().to_owned(), @@ -937,7 +938,8 @@ impl ApplicationProblem { legal_actions: vec![LegalAction::CorrectRequest], detail: Some(Box::new(detail)), }, - ApplicationProblemDetailV1::DiagnosticsPending { .. } => Self::Unavailable { + ApplicationProblemDetailV1::DiagnosticsPending { .. } + | ApplicationProblemDetailV1::DaemonUnreachable { .. } => Self::Unavailable { classification: ApplicationUnavailableClassV1::Authority, diagnostic, retry: RetryDirective::AfterDelay, diff --git a/crates/tracedecay-contracts/src/result/problem_detail.rs b/crates/tracedecay-contracts/src/result/problem_detail.rs index 3b33345dd0..6359bae04b 100644 --- a/crates/tracedecay-contracts/src/result/problem_detail.rs +++ b/crates/tracedecay-contracts/src/result/problem_detail.rs @@ -1 +1,3 @@ -pub use tracedecay_domain::{ApplicationProblemDetailV1, DiagnosticsSearchedTsconfigV1}; +pub use tracedecay_domain::{ + ApplicationProblemDetailV1, DaemonServiceUnitObservationV1, DiagnosticsSearchedTsconfigV1, +}; diff --git a/crates/tracedecay-daemon-control/src/lib.rs b/crates/tracedecay-daemon-control/src/lib.rs index 08407f96b1..bce416fb84 100644 --- a/crates/tracedecay-daemon-control/src/lib.rs +++ b/crates/tracedecay-daemon-control/src/lib.rs @@ -90,8 +90,8 @@ pub use service::{ installed_service_state, prepare_scoop_package_service, quiesce_installed_service_before_lease, refresh_installed_service_under_lease_with_state, restore_installed_service_after_update, restore_scoop_package_service, service_spec, service_spec_with_remote_tls, service_status, - socket_path_or_default, start_service, stop_service, unavailable_daemon_socket_message, - uninstall_service, verify_installed_service_quiesced_under_lease, + socket_path_or_default, start_service, stop_service, uninstall_service, + unreachable_daemon_detail, verify_installed_service_quiesced_under_lease, wait_for_installed_service_state, with_exclusive_maintenance_window, with_unavailable_daemon_advice, }; diff --git a/crates/tracedecay-daemon-control/src/service.rs b/crates/tracedecay-daemon-control/src/service.rs index 18bd4a8c1f..b04749b7ae 100644 --- a/crates/tracedecay-daemon-control/src/service.rs +++ b/crates/tracedecay-daemon-control/src/service.rs @@ -10,6 +10,7 @@ use sha2::Digest; use tracedecay_daemon_identity::DAEMON_AUTHORITY_UNAVAILABLE; use tracedecay_domain::errors::{Result, TraceDecayError}; +use tracedecay_domain::{ApplicationProblemDetailV1, DaemonServiceUnitObservationV1}; use tracedecay_runtime_core::DAEMON_SHUTDOWN_DEADLINE; use tracedecay_runtime_core::config::ProfileRoot; @@ -519,18 +520,20 @@ impl DaemonServiceState { /// The literal connect failure for `socket_path`, then the managed service as /// observed on disk. They are separate facts: `TRACEDECAY_DAEMON_SOCKET` can /// name a socket the installed service never serves. -pub fn unavailable_daemon_socket_message(profile: &ProfileRoot, socket_path: &Path) -> String { - let named_by = - if std::env::var_os(SOCKET_ENV).is_some_and(|path| Path::new(&path) == socket_path) { - format!(" named by {SOCKET_ENV}") - } else { - String::new() - }; - format!( - "TraceDecay daemon socket '{}'{named_by} is not available. {}", - socket_path.display(), - observed_service_unit(profile, socket_path) - ) +/// The typed refusal detail for a client that cannot reach `socket_path`: +/// the socket, the environment variable that named it, and the managed +/// service unit as this client observed it. +pub fn unreachable_daemon_detail( + profile: &ProfileRoot, + socket_path: &Path, +) -> ApplicationProblemDetailV1 { + ApplicationProblemDetailV1::DaemonUnreachable { + socket: socket_path.display().to_string(), + named_by: std::env::var_os(SOCKET_ENV) + .is_some_and(|path| Path::new(&path) == socket_path) + .then(|| SOCKET_ENV.to_owned()), + service_unit: observed_service_unit(profile), + } } /// Completes a missing-authority refusal with the advice for `socket_path`, @@ -546,7 +549,10 @@ pub fn with_unavailable_daemon_advice( TraceDecayError::project_route( code, true, - format!("{detail} {}", observed_service_unit(profile, socket_path)), + format!( + "{detail} {}", + observed_service_unit(profile).advice(&socket_path.display().to_string()) + ), ) } _ => error, @@ -556,7 +562,7 @@ pub fn with_unavailable_daemon_advice( /// Unit-file observation only. Connect-path diagnosis must not spawn /// `systemctl`, which races tests that fake PATH and is slower than a socket /// miss. -fn observed_service_unit(profile: &ProfileRoot, socket_path: &Path) -> String { +fn observed_service_unit(profile: &ProfileRoot) -> DaemonServiceUnitObservationV1 { let unit = service_unit_path(profile).and_then(|service_path| { if service_unit_exists(&service_path)? { Ok(Some((read_service_unit(&service_path)?, service_path))) @@ -565,27 +571,17 @@ fn observed_service_unit(profile: &ProfileRoot, socket_path: &Path) -> String { } }); match unit { - Err(error) => format!( - "This client cannot see whether a managed TraceDecay daemon service is installed ({error}). Check `tracedecay daemon status` before starting or installing a daemon." - ), - Ok(None) => "No managed TraceDecay daemon service is installed. Run `tracedecay daemon install-service` only if you want a managed daemon." - .to_string(), - Ok(Some((unit_text, service_path))) => { - let served = socket_path_from_unit_text(&unit_text) - .unwrap_or_else(|| default_socket_path_for_profile(profile.data_dir())); - if served == socket_path { - format!( - "The managed TraceDecay daemon service is installed at '{}' and serves this socket; it may be intentionally held, and passive clients do not start it. Check `tracedecay daemon status`, and run `tracedecay daemon start` only if you want it running.", - service_path.display() - ) - } else { - format!( - "The managed TraceDecay daemon service is installed at '{}' and serves '{}', not this socket.", - service_path.display(), - served.display() - ) - } - } + Err(error) => DaemonServiceUnitObservationV1::Unobservable { + error: error.to_string(), + }, + Ok(None) => DaemonServiceUnitObservationV1::NotInstalled, + Ok(Some((unit_text, service_path))) => DaemonServiceUnitObservationV1::Installed { + path: service_path.display().to_string(), + serves: socket_path_from_unit_text(&unit_text) + .unwrap_or_else(|| default_socket_path_for_profile(profile.data_dir())) + .display() + .to_string(), + }, } } diff --git a/crates/tracedecay-daemon-control/src/service/tests.rs b/crates/tracedecay-daemon-control/src/service/tests.rs index 674982d502..9a9fbaeda6 100644 --- a/crates/tracedecay-daemon-control/src/service/tests.rs +++ b/crates/tracedecay-daemon-control/src/service/tests.rs @@ -2459,12 +2459,23 @@ fn socket_advice_names_what_it_observed_about_the_unit() { let home = TempDir::new().unwrap(); let profile = ProfileRoot::under_home(home.path()); let socket = profile.data_dir().join("daemon.sock"); + let refusal = |profile: &ProfileRoot| { + let detail = super::unreachable_daemon_detail(profile, &socket); + (detail.clone(), detail.message()) + }; assert_eq!( - super::unavailable_daemon_socket_message(&profile, &socket), - format!( - "TraceDecay daemon socket '{}' is not available. No managed TraceDecay daemon service is installed. Run `tracedecay daemon install-service` only if you want a managed daemon.", - socket.display() + refusal(&profile), + ( + tracedecay_domain::ApplicationProblemDetailV1::DaemonUnreachable { + socket: socket.display().to_string(), + named_by: None, + service_unit: tracedecay_domain::DaemonServiceUnitObservationV1::NotInstalled, + }, + format!( + "TraceDecay daemon socket '{}' is not available. No managed TraceDecay daemon service is installed. Run `tracedecay daemon install-service` only if you want a managed daemon.", + socket.display() + ) ) ); @@ -2472,16 +2483,26 @@ fn socket_advice_names_what_it_observed_about_the_unit() { std::fs::create_dir_all(&unit_dir).unwrap(); std::fs::write(unit_dir.join(crate::SERVICE_NAME), "[Service]\n").unwrap(); assert_eq!( - super::unavailable_daemon_socket_message(&profile, &socket), - format!( - "TraceDecay daemon socket '{}' is not available. The managed TraceDecay daemon service is installed at '{}' and serves this socket; it may be intentionally held, and passive clients do not start it. Check `tracedecay daemon status`, and run `tracedecay daemon start` only if you want it running.", - socket.display(), - unit_dir.join(crate::SERVICE_NAME).display() + refusal(&profile), + ( + tracedecay_domain::ApplicationProblemDetailV1::DaemonUnreachable { + socket: socket.display().to_string(), + named_by: None, + service_unit: tracedecay_domain::DaemonServiceUnitObservationV1::Installed { + path: unit_dir.join(crate::SERVICE_NAME).display().to_string(), + serves: socket.display().to_string(), + }, + }, + format!( + "TraceDecay daemon socket '{}' is not available. The managed TraceDecay daemon service is installed at '{}' and serves this socket; it may be intentionally held, and passive clients do not start it. Check `tracedecay daemon status`, and run `tracedecay daemon start` only if you want it running.", + socket.display(), + unit_dir.join(crate::SERVICE_NAME).display() + ) ) ); assert_eq!( - super::unavailable_daemon_socket_message(&ProfileRoot::new(profile.data_dir()), &socket), + refusal(&ProfileRoot::new(profile.data_dir())).1, format!( "TraceDecay daemon socket '{}' is not available. This client cannot see whether a managed TraceDecay daemon service is installed (config error: could not determine XDG config directory). Check `tracedecay daemon status` before starting or installing a daemon.", socket.display() diff --git a/crates/tracedecay-daemon-service/src/application_surface.rs b/crates/tracedecay-daemon-service/src/application_surface.rs index f2f89b057b..5f88cfdc9f 100644 --- a/crates/tracedecay-daemon-service/src/application_surface.rs +++ b/crates/tracedecay-daemon-service/src/application_surface.rs @@ -73,7 +73,7 @@ pub use feedback_observation::observe_surface_argument_rejection; use handoff::router_with_executor as handoff_application_router_with_executor; use multi_root_http::router_with_executor as multi_root_application_router_with_executor; use operation_events::http_operation_event_router; -pub use problems::{map_dispatch_error, mcp_project_open_reset_refusal}; +pub use problems::{map_dispatch_error, settled_tool_refusal}; pub(crate) use registered_http::registered_executor_unavailable; use request_control::application_http_context; pub use workflow::invoke_workflow_operation; diff --git a/crates/tracedecay-daemon-service/src/application_surface/problems.rs b/crates/tracedecay-daemon-service/src/application_surface/problems.rs index e4332074b1..f1f3481db7 100644 --- a/crates/tracedecay-daemon-service/src/application_surface/problems.rs +++ b/crates/tracedecay-daemon-service/src/application_surface/problems.rs @@ -3,15 +3,14 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use tracedecay_contracts::{ - ApplicationContractError, ApplicationProblem, ApplicationProblemDetailV1, - ApplicationProblemEnvelope, LegalAction, ProblemOwningLayer, RequestId, ResultContractRef, - RetryDirective, SafeDiagnostic, + ApplicationContractError, ApplicationProblem, ApplicationProblemEnvelope, LegalAction, + ProblemOwningLayer, RequestId, ResultContractRef, RetryDirective, SafeDiagnostic, }; use tracedecay_daemon_protocol::{ ApplicationSurfaceAdapterError, CatalogBindingResolver, DispatchError, }; use tracedecay_domain::UtcMicros; -use tracedecay_tool_catalog::{ApplicationSurfaceOperation, BindingSurface, SchemaId}; +use tracedecay_tool_catalog::{ApplicationSurfaceOperation, BindingId, BindingSurface}; use super::catalog::{application_surface_catalog_ref, resolve_application_binding}; @@ -28,23 +27,14 @@ pub(super) fn registered_adapter_unavailable( code: &str, message: &str, ) -> Response { - let Ok(schema_id) = SchemaId::new("schema.tracedecay.http.adapter-problem.v1") else { - return StatusCode::SERVICE_UNAVAILABLE.into_response(); - }; - let Ok(contract) = ResultContractRef::new(schema_id, 1) else { - return StatusCode::SERVICE_UNAVAILABLE.into_response(); - }; - match ApplicationProblemEnvelope::new( - contract, + match tracedecay_api::adapter_problem( request_id, ApplicationProblem::unavailable(SafeDiagnostic { code: code.to_owned(), message: message.to_owned(), }), ) { - Ok(problem) => tracedecay_api::application_problem_response( - problem.with_owning_layer(ProblemOwningLayer::Adapter), - ), + Ok(problem) => tracedecay_api::application_problem_response(problem), Err(error) => application_contract_error_response(error), } } @@ -114,29 +104,29 @@ pub(super) fn http_adapter_problem( .map(|problem| problem.with_owning_layer(ProblemOwningLayer::Adapter)) } -/// The canonical typed terminal for an MCP `tools/call` whose project open -/// was refused because the store requires an explicit reset. +/// A refusal settled before any handler ran (a refused project open, an +/// unreachable daemon) as the typed problem envelope of `tool_name` under +/// its `surface` binding's result contract, with that binding's id. /// -/// 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, 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( +/// No route can hand such a call to its handler, so the truthful answer for +/// the named operation is this runtime-owned terminal. Returns `None` for +/// tools without a mounted application binding on `surface`. +pub fn settled_tool_refusal( + surface: BindingSurface, tool_name: &str, request_id: RequestId, - detail: ApplicationProblemDetailV1, -) -> Option { + problem: ApplicationProblem, +) -> Option<(BindingId, ApplicationProblemEnvelope)> { 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 binding = resolve_application_binding(&resolver, surface, operation)?; let contract = ResultContractRef::from_schema(&binding.result_schema); - let problem = ApplicationProblem::from_detail(detail); - ApplicationProblemEnvelope::new(contract, request_id, problem) - .ok() - .map(|envelope| envelope.with_owning_layer(ProblemOwningLayer::Runtime)) + let envelope = ApplicationProblemEnvelope::new(contract, request_id, problem).ok()?; + Some(( + binding.binding_id, + envelope.with_owning_layer(ProblemOwningLayer::Runtime), + )) } pub(crate) fn current_micros() -> Result { diff --git a/crates/tracedecay-daemon-service/src/logging.rs b/crates/tracedecay-daemon-service/src/logging.rs index e60db173d2..65419f9066 100644 --- a/crates/tracedecay-daemon-service/src/logging.rs +++ b/crates/tracedecay-daemon-service/src/logging.rs @@ -346,10 +346,10 @@ pub fn unavailable_error( profile: &tracedecay_runtime_core::config::ProfileRoot, socket_path: &Path, ) -> TraceDecayError { - TraceDecayError::project_route( + TraceDecayError::project_route_with_detail( tracedecay_daemon_protocol::DAEMON_CONNECT_DOWN, true, - tracedecay_daemon_control::unavailable_daemon_socket_message(profile, socket_path), + tracedecay_daemon_control::unreachable_daemon_detail(profile, socket_path), ) } diff --git a/crates/tracedecay-domain/src/application_problem_detail.rs b/crates/tracedecay-domain/src/application_problem_detail.rs index d1ca235ef9..ed6146340f 100644 --- a/crates/tracedecay-domain/src/application_problem_detail.rs +++ b/crates/tracedecay-domain/src/application_problem_detail.rs @@ -60,6 +60,31 @@ pub enum ApplicationProblemDetailV1 { producer: String, generation: Option, }, + /// No TraceDecay daemon accepts connections on `socket`. `named_by` is + /// the environment variable that chose the socket, when one did; + /// `service_unit` is what this client observed of the managed service. + DaemonUnreachable { + socket: String, + named_by: Option, + service_unit: DaemonServiceUnitObservationV1, + }, +} + +/// The managed daemon service unit as a client observed it, from the unit +/// file alone. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)] +pub enum DaemonServiceUnitObservationV1 { + NotInstalled, + /// The unit file could not be read. + Unobservable { + error: String, + }, + /// The unit file at `path` serves the socket `serves`. + Installed { + path: String, + serves: String, + }, } /// One tsconfig location the diagnostics owner search checked. @@ -126,6 +151,7 @@ impl ApplicationProblemDetailV1 { Self::ResetRequired { .. } => "application.reset-required", Self::DiagnosticsUnsupported { .. } => "application.diagnostics.unsupported", Self::DiagnosticsPending { .. } => "application.diagnostics.pending", + Self::DaemonUnreachable { .. } => "daemon.unreachable", } } @@ -194,6 +220,11 @@ impl ApplicationProblemDetailV1 { a complete generation. Retry shortly." ) } + Self::DaemonUnreachable { + socket, + named_by, + service_unit, + } => daemon_unreachable_sentence(socket, named_by.as_deref(), service_unit), }; let folded = crate::fold_control_characters(&text); crate::utf8_prefix_at_or_before(folded.trim(), MAX_RENDERED_MESSAGE_BYTES) @@ -284,10 +315,62 @@ impl ApplicationProblemDetailV1 { generation.clone().unwrap_or_else(|| "none".to_owned()), ), ], + // The unbounded sentence names the socket, the variable that chose + // it, and the observed unit together with the next step. + Self::DaemonUnreachable { + socket, + named_by, + service_unit, + } => vec![( + "Daemon unreachable", + daemon_unreachable_sentence(socket, named_by.as_deref(), service_unit), + )], } } } +impl DaemonServiceUnitObservationV1 { + /// What the observed unit means for a client that cannot reach `socket`. + pub fn advice(&self, socket: &str) -> String { + match self { + DaemonServiceUnitObservationV1::NotInstalled => { + "No managed TraceDecay daemon service is installed. Run `tracedecay \ + daemon install-service` only if you want a managed daemon." + .to_owned() + } + DaemonServiceUnitObservationV1::Unobservable { error } => format!( + "This client cannot see whether a managed TraceDecay daemon service is \ + installed ({error}). Check `tracedecay daemon status` before starting \ + or installing a daemon." + ), + DaemonServiceUnitObservationV1::Installed { path, serves } if serves == socket => { + format!( + "The managed TraceDecay daemon service is installed at '{path}' and \ + serves this socket; it may be intentionally held, and passive \ + clients do not start it. Check `tracedecay daemon status`, and run \ + `tracedecay daemon start` only if you want it running." + ) + } + DaemonServiceUnitObservationV1::Installed { path, serves } => format!( + "The managed TraceDecay daemon service is installed at '{path}' and \ + serves '{serves}', not this socket." + ), + } + } +} + +fn daemon_unreachable_sentence( + socket: &str, + named_by: Option<&str>, + service_unit: &DaemonServiceUnitObservationV1, +) -> String { + let named_by = named_by.map_or_else(String::new, |variable| format!(" named by {variable}")); + format!( + "TraceDecay daemon socket '{socket}'{named_by} is not available. {}", + service_unit.advice(socket) + ) +} + fn searched_list(searched: &[DiagnosticsSearchedTsconfigV1]) -> String { searched .iter() diff --git a/crates/tracedecay-domain/src/lib.rs b/crates/tracedecay-domain/src/lib.rs index 25e55f01e6..a065f68792 100644 --- a/crates/tracedecay-domain/src/lib.rs +++ b/crates/tracedecay-domain/src/lib.rs @@ -45,7 +45,9 @@ pub mod workflow_fan_out_census; pub mod workflow_receipt; pub mod workflow_run; -pub use application_problem_detail::{ApplicationProblemDetailV1, DiagnosticsSearchedTsconfigV1}; +pub use application_problem_detail::{ + ApplicationProblemDetailV1, DaemonServiceUnitObservationV1, DiagnosticsSearchedTsconfigV1, +}; pub use automation::{SESSION_EVIDENCE_BUDGET_EXHAUSTED, SESSION_EVIDENCE_BUDGET_SUPPRESSED}; pub use canonical_text::{encode_lowercase_hex, nonnegative_sha256_prefix, sha256_hex_suffix}; pub use code_intelligence::{ diff --git a/crates/tracedecay-mcp/src/application_output/tool_result.rs b/crates/tracedecay-mcp/src/application_output/tool_result.rs index 2a4469c882..52f993ff1b 100644 --- a/crates/tracedecay-mcp/src/application_output/tool_result.rs +++ b/crates/tracedecay-mcp/src/application_output/tool_result.rs @@ -72,18 +72,9 @@ pub fn render_application_result( result: &ApplicationResult, requested_format: RequestedOutputFormat, ) -> Result { - let (value, failure_message) = match result { - Ok(application) => (serde_json::to_value(application)?, None), - Err(problem) => { - let failure_message = match problem.problem.kind() { - ApplicationProblemKind::NotFoundOrNotAuthorized => { - "application surface was not found or is not authorized" - } - ApplicationProblemKind::Unavailable => "application surface unavailable", - _ => "application surface request failed", - }; - (serde_json::to_value(problem)?, Some(failure_message)) - } + let value = match result { + Ok(application) => serde_json::to_value(application)?, + Err(problem) => serde_json::to_value(problem)?, }; let markdown = match requested_format { RequestedOutputFormat::Json => None, @@ -95,30 +86,42 @@ pub fn render_application_result( let text = finalize_with_format(response_handle_root, requested_format, &value, || { markdown.unwrap_or_default() }); - let mut rendered = text_tool_result(&text, Vec::new()); match result { - Ok(envelope) => ResponseTrailer { - touched_files: &envelope.touched_files, - code_graph: envelope.code_graph.as_ref(), - cost: envelope.cost.as_ref(), - } - .attach(&mut rendered), - // Markdown alone would strand the problem in prose no client can - // classify; the legal actions, retry directive, detail, and any - // committed receipt are what a caller acts on. - Err(problem) => { - if let Some(object) = rendered.value.as_object_mut() { - object.insert( - "structuredContent".to_string(), - problem_structured_content(&problem.problem)?, - ); + Ok(envelope) => { + let mut rendered = text_tool_result(&text, Vec::new()); + ResponseTrailer { + touched_files: &envelope.touched_files, + code_graph: envelope.code_graph.as_ref(), + cost: envelope.cost.as_ref(), } + .attach(&mut rendered); + Ok(rendered) } + Err(problem) => problem_tool_result(&text, problem), + } +} + +/// The one refusing tool result: `text` beside the typed record at +/// `structuredContent.problem`, marked as a semantic failure. Markdown alone +/// would strand the problem in prose no client can classify; the legal +/// actions, retry directive, detail, and any committed receipt are what a +/// caller acts on. +pub fn problem_tool_result(text: &str, problem: &ApplicationProblemEnvelope) -> Result { + let failure_message = match problem.problem.kind() { + ApplicationProblemKind::NotFoundOrNotAuthorized => { + "application surface was not found or is not authorized" + } + ApplicationProblemKind::Unavailable => "application surface unavailable", + _ => "application surface request failed", + }; + let mut rendered = text_tool_result(text, Vec::new()); + if let Some(object) = rendered.value.as_object_mut() { + object.insert( + "structuredContent".to_string(), + problem_structured_content(&problem.problem)?, + ); } - Ok(match failure_message { - Some(failure_message) => rendered - .with_semantic_error(true) - .with_failure_message(failure_message), - None => rendered, - }) + Ok(rendered + .with_semantic_error(true) + .with_failure_message(failure_message)) } diff --git a/crates/tracedecay-mcp/src/tool_errors.rs b/crates/tracedecay-mcp/src/tool_errors.rs index d06253381f..6f5f71898c 100644 --- a/crates/tracedecay-mcp/src/tool_errors.rs +++ b/crates/tracedecay-mcp/src/tool_errors.rs @@ -180,11 +180,9 @@ pub fn project_route_problem_kind(reason_code: &str) -> Option<&'static str> { } } -/// The typed problem a project-route refusal of `tool_name` carries: the -/// JSON-RPC error `data` of an MCP tool call, and the `problem` member -/// `tracedecay tool` prints for a JSON request. `None` for any other error. -#[must_use] -pub fn project_route_problem(tool_name: &str, error: &TraceDecayError) -> Option { +/// The JSON-RPC error `data` a project-route refusal of `tool_name` carries. +/// `None` for any other error. +fn project_route_problem(tool_name: &str, error: &TraceDecayError) -> Option { let (reason_code, retryable, detail) = error.project_route_context()?; let mut data = json!({ "tool": tool_name, diff --git a/crates/tracedecay/src/daemon/project_open_handshake.rs b/crates/tracedecay/src/daemon/project_open_handshake.rs index 4e7795456d..439740e0cd 100644 --- a/crates/tracedecay/src/daemon/project_open_handshake.rs +++ b/crates/tracedecay/src/daemon/project_open_handshake.rs @@ -255,8 +255,11 @@ pub(super) fn tool_call_open_refusal_response( 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 envelope = tracedecay_daemon_service::application_surface::mcp_project_open_reset_refusal( - tool_name, request_id, detail, + let (_, envelope) = tracedecay_daemon_service::application_surface::settled_tool_refusal( + tracedecay_tool_catalog::BindingSurface::Mcp, + tool_name, + request_id, + tracedecay_contracts::ApplicationProblem::from_detail(detail), )?; let text = serde_json::to_string(&envelope).ok()?; let structured_content = diff --git a/crates/tracedecay/src/mcp/tools/handlers/application_surface.rs b/crates/tracedecay/src/mcp/tools/handlers/application_surface.rs index 771914c66f..45f56547e4 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/application_surface.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/application_surface.rs @@ -522,6 +522,59 @@ fn settle_graph_tool_result( )) } +/// A route refusal that settled before any owner answered `tool_name` (an +/// unreachable daemon, a refused argument, an unknown tool), rendered as the +/// one tool-result shape every route answers: `isError` with the typed +/// record at `structuredContent.problem`. A tool with a binding on `surface` +/// refuses under its own result contract; a name no binding owns refuses +/// under the adapter's. `None` when `error` is not a route refusal. +pub fn render_settled_route_refusal( + surface: tracedecay_tool_catalog::BindingSurface, + tool_name: &str, + request_id: RequestId, + error: &TraceDecayError, + args: &Value, +) -> Option> { + error.project_route_context()?; + let problem = graph_tool_error_problem(error); + let bound = ApplicationSurfaceOperation::from_tool_name(tool_name).and_then(|operation| { + tracedecay_daemon_service::application_surface::settled_tool_refusal( + surface, + tool_name, + request_id.clone(), + problem.clone(), + ) + .map(|(binding_id, problem)| ApplicationRefusal { + operation, + binding_id, + problem, + }) + }); + let rendered = match bound { + Some(refusal) => refusal.render(None, args), + None => unbound_refusal(request_id, problem), + }; + Some(rendered.map(|mut rendered| { + tracedecay_mcp::tool_errors::mark_semantic_tool_error(&mut rendered); + rendered + })) +} + +fn unbound_refusal( + request_id: RequestId, + problem: tracedecay_contracts::ApplicationProblem, +) -> Result { + let envelope = tracedecay_api::adapter_problem(request_id, problem).map_err(|error| { + TraceDecayError::Config { + message: format!("the adapter refusal violated its problem contract: {error}"), + } + })?; + tracedecay_mcp::application_output::tool_result::problem_tool_result( + &serde_json::to_string(&envelope)?, + &envelope, + ) +} + /// The graph-tool owner reports handler argument errors as invalid requests, /// a typed route detail or a lock that missed its deadline as that detail, a /// route refusal as unavailable under its own reason code, and any other diff --git a/crates/tracedecay/src/mcp/tools/handlers/mod.rs b/crates/tracedecay/src/mcp/tools/handlers/mod.rs index 4ceb49e09a..956c86cc02 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/mod.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/mod.rs @@ -9,7 +9,8 @@ pub(crate) use application_surface::graph_tool_error_problem; pub use application_surface::{ GraphToolOutcome, RetainedSurfaceExecution, execute_graph_tool_surface, execute_retained_surface_tool, handle_application_surface, render_application_surface_result, - render_retained_execution, retained_tool_target, run_retained_surface_tool, + render_retained_execution, render_settled_route_refusal, retained_tool_target, + run_retained_surface_tool, }; pub(crate) use dispatch_groups::compute_graph_tool_for_owner; pub use support::{registered_project_not_found, registered_project_selector_id}; diff --git a/crates/tracedecay/src/mcp/tools/mod.rs b/crates/tracedecay/src/mcp/tools/mod.rs index c4960d6f2a..b454b90cca 100644 --- a/crates/tracedecay/src/mcp/tools/mod.rs +++ b/crates/tracedecay/src/mcp/tools/mod.rs @@ -12,6 +12,6 @@ pub use handlers::{ execute_workflow_tool_surface, handle_application_surface, handle_tool_call, handle_tool_call_with_registry_options, registered_project_not_found, registered_project_selector_id, render_application_surface_result, render_retained_execution, - retained_tool_target, run_retained_surface_tool, + render_settled_route_refusal, retained_tool_target, run_retained_surface_tool, }; pub(crate) use handlers::{compute_graph_tool_for_owner, graph_tool_error_problem}; diff --git a/crates/tracedecay/tests/daemon_suite/code_index_park_test.rs b/crates/tracedecay/tests/daemon_suite/code_index_park_test.rs index 91635b9961..df793adf83 100644 --- a/crates/tracedecay/tests/daemon_suite/code_index_park_test.rs +++ b/crates/tracedecay/tests/daemon_suite/code_index_park_test.rs @@ -157,7 +157,10 @@ async fn parked_worktree_queries_carry_the_park_and_are_not_retryable() { let cli = cli_symbol_search(environment.home(), &project, &request, true); assert_eq!( - (&cli["problem"]["detail"], &cli["problem"]["message"]), + ( + &cli["structuredContent"]["problem"]["detail"], + &cli["structuredContent"]["problem"]["message"], + ), (&parked_detail, &problem["message"]), "CLI --json: {cli}" ); @@ -221,8 +224,8 @@ fn cli_symbol_search_output(home: &Path, project: &Path, request: &Value, json: fn cli_symbol_search(home: &Path, project: &Path, request: &Value, json: bool) -> Value { let stdout = cli_symbol_search_output(home, project, request, json); - serde_json::from_str(stdout.lines().next().unwrap_or_default()) - .unwrap_or_else(|error| panic!("CLI --json printed no envelope ({error}):\n{stdout}")) + serde_json::from_str(&stdout) + .unwrap_or_else(|error| panic!("CLI --json printed no refusal ({error}):\n{stdout}")) } fn cli_symbol_search_text(home: &Path, project: &Path, request: &Value) -> String { diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/advisory_runtime_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/advisory_runtime_acceptance.rs index 8a9a6d2d1c..44237aaf0e 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/advisory_runtime_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/advisory_runtime_acceptance.rs @@ -1231,8 +1231,9 @@ async fn packaged_host_ingest_delivers_a_registered_advisory_cycle() { serde_json::from_str::(&stdout) .ok() .is_some_and(|response| { - response["problem"]["code"] == "feedback.advisory-cycle.unavailable" - && response["problem"]["retryable"] == true + let problem = &response["structuredContent"]["problem"]; + problem["code"] == "feedback.advisory-cycle.unavailable" + && problem["retryable"] == true }); assert!( retryable_unavailable, diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/application_production_reachability.rs b/crates/tracedecay/tests/runtime_acceptance_suite/application_production_reachability.rs index 18488afa9e..2a7a37fa4c 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/application_production_reachability.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/application_production_reachability.rs @@ -512,6 +512,26 @@ fn invocation_terminal(result: &ApplicationSurfaceInvocationResult) -> Value { } } +/// The application envelope a CLI `--json` call printed: the envelope +/// itself for an answer, and for a refusal the tool result's envelope text, +/// whose record is the one at `structuredContent.problem`. +fn cli_terminal(printed: Value) -> Value { + if printed["isError"] != true { + return printed; + } + let envelope: Value = serde_json::from_str( + printed["content"][0]["text"] + .as_str() + .unwrap_or_else(|| panic!("CLI refusal names its envelope: {printed:#}")), + ) + .unwrap_or_else(|error| panic!("CLI refusal envelope JSON ({error}): {printed:#}")); + assert_eq!( + envelope["problem"], printed["structuredContent"]["problem"], + "the refusal's structured record is its envelope's: {printed:#}" + ); + envelope +} + fn terminal_disposition(value: &Value) -> (&str, &str) { if let Some(outcome) = value.get("outcome") { let outcome = outcome["outcome"] @@ -919,6 +939,7 @@ async fn operation_family_executes_through_cli_mcp_and_http() { ); let cli: Value = serde_json::from_slice(&cli.stdout) .unwrap_or_else(|error| panic!("parse CLI {} JSON: {error}", operation.as_str())); + let cli = cli_terminal(cli); assert_eq!( mcp.binding_id.as_str(), diff --git a/crates/tracedecay/tests/transport_acceptance_suite/typed_terminal_restart_acceptance.rs b/crates/tracedecay/tests/transport_acceptance_suite/typed_terminal_restart_acceptance.rs index e6093fd070..00d9900b9e 100644 --- a/crates/tracedecay/tests/transport_acceptance_suite/typed_terminal_restart_acceptance.rs +++ b/crates/tracedecay/tests/transport_acceptance_suite/typed_terminal_restart_acceptance.rs @@ -523,7 +523,7 @@ fn reset_required_survives_physical_daemon_restart_via_cli() { "tracedecay_storage_status", &json!({ "include_details": false }), ); - assert_reset_required(&reset_payload, "first observation"); + assert_reset_required(&reset_payload["structuredContent"], "first observation"); // Replace the serving process again. The reset-only legal action must be // reported identically: the tampered store is never repaired implicitly, @@ -546,7 +546,10 @@ fn reset_required_survives_physical_daemon_restart_via_cli() { "tracedecay_storage_status", &json!({ "include_details": false }), ); - assert_reset_required(&reset_payload_after_restart, "after a physical restart"); + assert_reset_required( + &reset_payload_after_restart["structuredContent"], + "after a physical restart", + ); let _ = daemon.kill_and_wait(); } diff --git a/dashboard/codegen/schemas/dashboard-contracts.schema.json b/dashboard/codegen/schemas/dashboard-contracts.schema.json index 6b777e1860..3300ffcf55 100644 --- a/dashboard/codegen/schemas/dashboard-contracts.schema.json +++ b/dashboard/codegen/schemas/dashboard-contracts.schema.json @@ -1543,6 +1543,35 @@ "generation" ], "type": "object" + }, + { + "additionalProperties": false, + "description": "No TraceDecay daemon accepts connections on `socket`. `named_by` is\nthe environment variable that chose the socket, when one did;\n`service_unit` is what this client observed of the managed service.", + "properties": { + "kind": { + "const": "daemon_unreachable", + "type": "string" + }, + "named_by": { + "type": [ + "string", + "null" + ] + }, + "service_unit": { + "$ref": "#/$defs/DaemonServiceUnitObservationV1" + }, + "socket": { + "type": "string" + } + }, + "required": [ + "kind", + "socket", + "named_by", + "service_unit" + ], + "type": "object" } ] }, @@ -4799,6 +4828,64 @@ ], "type": "object" }, + "DaemonServiceUnitObservationV1": { + "description": "The managed daemon service unit as a client observed it, from the unit\nfile alone.", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "state": { + "const": "not_installed", + "type": "string" + } + }, + "required": [ + "state" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "The unit file could not be read.", + "properties": { + "error": { + "type": "string" + }, + "state": { + "const": "unobservable", + "type": "string" + } + }, + "required": [ + "state", + "error" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "The unit file at `path` serves the socket `serves`.", + "properties": { + "path": { + "type": "string" + }, + "serves": { + "type": "string" + }, + "state": { + "const": "installed", + "type": "string" + } + }, + "required": [ + "state", + "path", + "serves" + ], + "type": "object" + } + ] + }, "DashboardAuthorizationV1": { "description": "Authorization outcome for the read. On the loopback single-user dashboard a\nlegal local read is [`Self::Authorized`]; the other variants are retained so\nthe contract can express `unauthorized` (identity absent/expired), `denied`\n(known identity lacks permission), and `redacted` reads without a schema\nchange.", "oneOf": [ diff --git a/dashboard/src/contracts/decoders.ts b/dashboard/src/contracts/decoders.ts index 28b5cf793f..bd2f804c88 100644 --- a/dashboard/src/contracts/decoders.ts +++ b/dashboard/src/contracts/decoders.ts @@ -151,6 +151,7 @@ import type { CredentialReferenceId, CrossMergeModeV1, CrossMergePolicyV1, + DaemonServiceUnitObservationV1, DashboardAuthorizationV1, DashboardCoverageCompletenessV1, DashboardCoverageV1, @@ -1495,6 +1496,11 @@ export const ApplicationExecutionFailureClassV1Schema: z.ZodEnum<["denied", "mal /** The structured facts behind a problem. Adapters read these fields; the problem's `message` is only their one human rendering. */ export const ApplicationProblemDetailV1Schema: z.ZodType = z.discriminatedUnion("kind", [z.object({ + kind: z.literal("daemon_unreachable"), + named_by: z.string().nullable(), + service_unit: z.lazy(() => DaemonServiceUnitObservationV1Schema), + socket: z.string(), +}).strict(), z.object({ generation: z.string().nullable(), kind: z.literal("diagnostics_pending"), producer: z.string(), @@ -2716,6 +2722,19 @@ export const CrossMergePolicyV1Schema: z.ZodObject<{ default_mode: z.lazy(() => CrossMergeModeV1Schema), }).strict(); +/** The managed daemon service unit as a client observed it, from the unit +file alone. */ +export const DaemonServiceUnitObservationV1Schema: z.ZodType = z.discriminatedUnion("state", [z.object({ + path: z.string(), + serves: z.string(), + state: z.literal("installed"), +}).strict(), z.object({ + state: z.literal("not_installed"), +}).strict(), z.object({ + error: z.string(), + state: z.literal("unobservable"), +}).strict()]); + /** Authorization outcome for the read. On the loopback single-user dashboard a legal local read is [`Self::Authorized`]; the other variants are retained so the contract can express `unauthorized` (identity absent/expired), `denied` diff --git a/dashboard/src/contracts/types.ts b/dashboard/src/contracts/types.ts index eb12bcbdd9..487a0857ed 100644 --- a/dashboard/src/contracts/types.ts +++ b/dashboard/src/contracts/types.ts @@ -321,6 +321,12 @@ export type AnalyticsUsageSummaryV1 = { export type ApplicationExecutionFailureClassV1 = "denied" | "malformed_output" | "permanent"; export type ApplicationProblemDetailV1 = + | { + kind: "daemon_unreachable"; + named_by: string | null; + service_unit: DaemonServiceUnitObservationV1; + socket: string; + } | { generation: string | null; kind: "diagnostics_pending"; @@ -1041,6 +1047,20 @@ export type CrossMergePolicyV1 = { default_mode: CrossMergeModeV1; }; +export type DaemonServiceUnitObservationV1 = + | { + path: string; + serves: string; + state: "installed"; + } + | { + state: "not_installed"; + } + | { + error: string; + state: "unobservable"; + }; + export type DashboardAuthorizationV1 = | { outcome: "authorized"; diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index b9ee97dd1a..cc6cc4d35f 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@tracedecay/sdk", - "version": "0.9.0", + "version": "0.10.0", "type": "module", "description": "Generated TraceDecay API contracts with a strict lifecycle client.", "author": "ScriptedAlchemy", diff --git a/sdks/typescript/src/operations.ts b/sdks/typescript/src/operations.ts index f3c06c1887..55e5049eb1 100644 --- a/sdks/typescript/src/operations.ts +++ b/sdks/typescript/src/operations.ts @@ -102,7 +102,7 @@ export type AnalyzerRestartPolicyV1 = "restart_on_configuration_change" | "manua export type AnalyzerSettingsV1 = { readonly schema_version: number; readonly selections: readonly AnalyzerLanguageSelectionV1[] }; export type AnalyzerStructuredValueV1 = { readonly kind: "boolean"; readonly value: boolean; readonly [key: string]: unknown } | { readonly kind: "integer"; readonly value: number; readonly [key: string]: unknown } | { readonly kind: "text"; readonly value: string; readonly [key: string]: unknown } | { readonly kind: "text_list"; readonly value: readonly string[]; readonly [key: string]: unknown } | { readonly kind: "object"; readonly value: { readonly [key: string]: AnalyzerStructuredValueV1 }; readonly [key: string]: unknown }; export type AnchorExpandResult = { readonly anchors: readonly RetrievalAnchorId[] }; -export type ApplicationProblemDetailV1_2 = { readonly cause: string; readonly kind: "parked"; readonly remedy: string; readonly retries_on_wake: boolean } | { readonly active: number; readonly committed: number; readonly kind: "stale_refresh_frontier"; readonly requested: number } | { readonly current: number; readonly field: string; readonly kind: "stale_precondition"; readonly requested: number } | { readonly deadline_ms: number; readonly kind: "lock_deadline"; readonly resource: string } | { readonly authority: string; readonly found_version?: number | null; readonly kind: "reset_required"; readonly reason: string; readonly remedy: string; readonly required_version?: number | null } | { readonly file?: string | null; readonly kind: "diagnostics_unsupported"; readonly searched: readonly DiagnosticsSearchedTsconfigV1[] } | { readonly generation?: string | null; readonly kind: "diagnostics_pending"; readonly producer: string }; +export type ApplicationProblemDetailV1_2 = { readonly cause: string; readonly kind: "parked"; readonly remedy: string; readonly retries_on_wake: boolean } | { readonly active: number; readonly committed: number; readonly kind: "stale_refresh_frontier"; readonly requested: number } | { readonly current: number; readonly field: string; readonly kind: "stale_precondition"; readonly requested: number } | { readonly deadline_ms: number; readonly kind: "lock_deadline"; readonly resource: string } | { readonly authority: string; readonly found_version?: number | null; readonly kind: "reset_required"; readonly reason: string; readonly remedy: string; readonly required_version?: number | null } | { readonly file?: string | null; readonly kind: "diagnostics_unsupported"; readonly searched: readonly DiagnosticsSearchedTsconfigV1[] } | { readonly generation?: string | null; readonly kind: "diagnostics_pending"; readonly producer: string } | { readonly kind: "daemon_unreachable"; readonly named_by?: string | null; readonly service_unit: DaemonServiceUnitObservationV1; readonly socket: string }; export type ApplyWorkRelationReplanRequestV1 = { readonly mutation: WorkProductMutationIdentityV1; readonly proposal_id: ProposalId; readonly selection: WorkProductSelectionScopeV1 }; export type AstGrepResult = { readonly diff?: string | null; readonly dry_run?: boolean; readonly file_path: string; readonly message: string; readonly pattern: string; readonly rewrite: string; readonly success: boolean; readonly [key: string]: unknown }; export type AstGrepSearchMatchV1 = { readonly column: number; readonly file: string; readonly lang: string; readonly line: number; readonly line_text: string; readonly match: string }; @@ -305,6 +305,7 @@ export type CrossMergeModeV1 = "disabled" | "manual_receipt_only" | "fast_forwar export type CrossMergePolicyV1 = { readonly allow_cross_repository: boolean; readonly allowed_modes: readonly CrossMergeModeV1[]; readonly default_mode: CrossMergeModeV1 }; export type CurrentRemoteAuthorityStateV1 = { readonly state: "available"; readonly value: CurrentRemoteAuthorityV1; readonly [key: string]: unknown } | { readonly state: "partial"; readonly value: { readonly known_fence?: RemoteWriterFenceV1 | null; readonly missing: readonly RemoteAuthorityUnavailableReasonV1[]; readonly observed_at: UtcMicros; readonly [key: string]: unknown }; readonly [key: string]: unknown } | { readonly state: "unavailable"; readonly value: { readonly observed_at: UtcMicros; readonly reason: RemoteAuthorityUnavailableReasonV1; readonly [key: string]: unknown }; readonly [key: string]: unknown }; export type CurrentRemoteAuthorityV1 = { readonly credential_revision: number; readonly fence: RemoteWriterFenceV1; readonly observed_at: UtcMicros }; +export type DaemonServiceUnitObservationV1 = { readonly state: "not_installed" } | { readonly error: string; readonly state: "unobservable" } | { readonly path: string; readonly serves: string; readonly state: "installed" }; export type DashboardActionV1 = "start" | "stop"; export type DeadCodeSymbolV1 = { readonly file: string; readonly id: string; readonly kind: string; readonly line: number; readonly name: string; readonly signature?: string | null }; export type Deadline = { readonly expires_at: UtcMicros }; @@ -1826,7 +1827,7 @@ const DEFINITIONS = { AnalyzerSettingsV1: {"additionalProperties":false,"description":"Canonical analyzer settings. A changed selection produces a new\nconfiguration revision/digest; cache invalidation remains owned elsewhere.","properties":{"schema_version":{"format":"uint16","maximum":65535,"minimum":0,"type":"integer"},"selections":{"items":{"$ref":"#/$defs/AnalyzerLanguageSelectionV1"},"type":"array"}},"required":["schema_version","selections"],"type":"object"}, AnalyzerStructuredValueV1: {"description":"A structured analyzer option value. This deliberately excludes raw\nenvironment values, commands, credential material, and transport blobs.","oneOf":[{"properties":{"kind":{"const":"boolean","type":"string"},"value":{"type":"boolean"}},"required":["kind","value"],"type":"object"},{"properties":{"kind":{"const":"integer","type":"string"},"value":{"format":"int64","type":"integer"}},"required":["kind","value"],"type":"object"},{"properties":{"kind":{"const":"text","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"properties":{"kind":{"const":"text_list","type":"string"},"value":{"items":{"type":"string"},"type":"array"}},"required":["kind","value"],"type":"object"},{"properties":{"kind":{"const":"object","type":"string"},"value":{"additionalProperties":{"$ref":"#/$defs/AnalyzerStructuredValueV1"},"type":"object"}},"required":["kind","value"],"type":"object"}]}, AnchorExpandResult: {"additionalProperties":false,"properties":{"anchors":{"items":{"$ref":"#/$defs/RetrievalAnchorId"},"type":"array"}},"required":["anchors"],"type":"object"}, - ApplicationProblemDetailV1_2: {"description":"The structured facts behind a problem. Adapters read these fields; the\nproblem's `message` is only their one human rendering.","oneOf":[{"additionalProperties":false,"description":"The worktree's code index is parked until the operator applies\n`remedy`; repeating the request cannot change the answer.","properties":{"cause":{"type":"string"},"kind":{"const":"parked","type":"string"},"remedy":{"type":"string"},"retries_on_wake":{"type":"boolean"}},"required":["kind","cause","remedy","retries_on_wake"],"type":"object"},{"additionalProperties":false,"description":"A session refresh asked to begin from a source frontier the\ncommitted projection has already passed.","properties":{"active":{"format":"uint64","minimum":0,"type":"integer"},"committed":{"format":"uint64","minimum":0,"type":"integer"},"kind":{"const":"stale_refresh_frontier","type":"string"},"requested":{"format":"uint64","minimum":0,"type":"integer"}},"required":["kind","requested","committed","active"],"type":"object"},{"additionalProperties":false,"description":"A compare-and-swap request named `requested` in `field`, but the\nauthority holds `current`. Resending with `current` is a new request.","properties":{"current":{"format":"uint64","minimum":0,"type":"integer"},"field":{"type":"string"},"kind":{"const":"stale_precondition","type":"string"},"requested":{"format":"uint64","minimum":0,"type":"integer"}},"required":["kind","field","requested","current"],"type":"object"},{"additionalProperties":false,"description":"A writer lock stayed held by other writers past its admission\ndeadline.","properties":{"deadline_ms":{"format":"uint64","minimum":0,"type":"integer"},"kind":{"const":"lock_deadline","type":"string"},"resource":{"type":"string"}},"required":["kind","resource","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","reason","remedy"],"type":"object"},{"additionalProperties":false,"description":"No compiler runs automatically for the diagnostics scope: no tsconfig\nowns `file`, or, for a workspace read (`file` null), none exists under\nthe project root. `searched` lists the owner search's candidates,\nnearest first; it is empty for a workspace read or a file outside the\nproject root.","properties":{"file":{"type":["string","null"]},"kind":{"const":"diagnostics_unsupported","type":"string"},"searched":{"items":{"$ref":"#/$defs/DiagnosticsSearchedTsconfigV1"},"type":"array"}},"required":["kind","searched"],"type":"object"},{"additionalProperties":false,"description":"The TypeScript `producer` has not published diagnostics for the\ncurrent code generation yet. `generation` is the last generation it\npublished, null when it has published none.","properties":{"generation":{"type":["string","null"]},"kind":{"const":"diagnostics_pending","type":"string"},"producer":{"type":"string"}},"required":["kind","producer"],"type":"object"}]}, + ApplicationProblemDetailV1_2: {"description":"The structured facts behind a problem. Adapters read these fields; the\nproblem's `message` is only their one human rendering.","oneOf":[{"additionalProperties":false,"description":"The worktree's code index is parked until the operator applies\n`remedy`; repeating the request cannot change the answer.","properties":{"cause":{"type":"string"},"kind":{"const":"parked","type":"string"},"remedy":{"type":"string"},"retries_on_wake":{"type":"boolean"}},"required":["kind","cause","remedy","retries_on_wake"],"type":"object"},{"additionalProperties":false,"description":"A session refresh asked to begin from a source frontier the\ncommitted projection has already passed.","properties":{"active":{"format":"uint64","minimum":0,"type":"integer"},"committed":{"format":"uint64","minimum":0,"type":"integer"},"kind":{"const":"stale_refresh_frontier","type":"string"},"requested":{"format":"uint64","minimum":0,"type":"integer"}},"required":["kind","requested","committed","active"],"type":"object"},{"additionalProperties":false,"description":"A compare-and-swap request named `requested` in `field`, but the\nauthority holds `current`. Resending with `current` is a new request.","properties":{"current":{"format":"uint64","minimum":0,"type":"integer"},"field":{"type":"string"},"kind":{"const":"stale_precondition","type":"string"},"requested":{"format":"uint64","minimum":0,"type":"integer"}},"required":["kind","field","requested","current"],"type":"object"},{"additionalProperties":false,"description":"A writer lock stayed held by other writers past its admission\ndeadline.","properties":{"deadline_ms":{"format":"uint64","minimum":0,"type":"integer"},"kind":{"const":"lock_deadline","type":"string"},"resource":{"type":"string"}},"required":["kind","resource","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","reason","remedy"],"type":"object"},{"additionalProperties":false,"description":"No compiler runs automatically for the diagnostics scope: no tsconfig\nowns `file`, or, for a workspace read (`file` null), none exists under\nthe project root. `searched` lists the owner search's candidates,\nnearest first; it is empty for a workspace read or a file outside the\nproject root.","properties":{"file":{"type":["string","null"]},"kind":{"const":"diagnostics_unsupported","type":"string"},"searched":{"items":{"$ref":"#/$defs/DiagnosticsSearchedTsconfigV1"},"type":"array"}},"required":["kind","searched"],"type":"object"},{"additionalProperties":false,"description":"The TypeScript `producer` has not published diagnostics for the\ncurrent code generation yet. `generation` is the last generation it\npublished, null when it has published none.","properties":{"generation":{"type":["string","null"]},"kind":{"const":"diagnostics_pending","type":"string"},"producer":{"type":"string"}},"required":["kind","producer"],"type":"object"},{"additionalProperties":false,"description":"No TraceDecay daemon accepts connections on `socket`. `named_by` is\nthe environment variable that chose the socket, when one did;\n`service_unit` is what this client observed of the managed service.","properties":{"kind":{"const":"daemon_unreachable","type":"string"},"named_by":{"type":["string","null"]},"service_unit":{"$ref":"#/$defs/DaemonServiceUnitObservationV1"},"socket":{"type":"string"}},"required":["kind","socket","service_unit"],"type":"object"}]}, ApplyWorkRelationReplanRequestV1: {"additionalProperties":false,"properties":{"mutation":{"$ref":"#/$defs/WorkProductMutationIdentityV1"},"proposal_id":{"$ref":"#/$defs/ProposalId"},"selection":{"$ref":"#/$defs/WorkProductSelectionScopeV1"}},"required":["selection","proposal_id","mutation"],"type":"object"}, AstGrepResult: {"description":"Result of an ast-grep rewrite operation.","properties":{"diff":{"description":"Bounded preview of the would-be change. Populated only on a successful\ndry run; `None` for real edits and for failures.","type":["string","null"]},"dry_run":{"description":"True when this was a dry run: the rewrite was resolved (via the built-in\nliteral fallback or an ast-grep preview run) but nothing was written.","type":"boolean"},"file_path":{"type":"string"},"message":{"type":"string"},"pattern":{"type":"string"},"rewrite":{"type":"string"},"success":{"type":"boolean"}},"required":["success","file_path","pattern","rewrite","message"],"type":"object"}, AstGrepSearchMatchV1: {"additionalProperties":false,"properties":{"column":{"format":"uint32","minimum":0,"type":"integer"},"file":{"type":"string"},"lang":{"type":"string"},"line":{"format":"uint32","minimum":0,"type":"integer"},"line_text":{"type":"string"},"match":{"type":"string"}},"required":["file","line","column","lang","match","line_text"],"type":"object"}, @@ -2029,6 +2030,7 @@ const DEFINITIONS = { CrossMergePolicyV1: {"additionalProperties":false,"properties":{"allow_cross_repository":{"type":"boolean"},"allowed_modes":{"items":{"$ref":"#/$defs/CrossMergeModeV1"},"type":"array","uniqueItems":true},"default_mode":{"$ref":"#/$defs/CrossMergeModeV1"}},"required":["allowed_modes","default_mode","allow_cross_repository"],"type":"object"}, CurrentRemoteAuthorityStateV1: {"description":"Truthful authority lookup state. Missing evidence is never represented as\nan available authority or a successful empty response.","oneOf":[{"properties":{"state":{"const":"available","type":"string"},"value":{"$ref":"#/$defs/CurrentRemoteAuthorityV1"}},"required":["state","value"],"type":"object"},{"properties":{"state":{"const":"partial","type":"string"},"value":{"properties":{"known_fence":{"anyOf":[{"$ref":"#/$defs/RemoteWriterFenceV1"},{"type":"null"}]},"missing":{"items":{"$ref":"#/$defs/RemoteAuthorityUnavailableReasonV1"},"type":"array","uniqueItems":true},"observed_at":{"$ref":"#/$defs/UtcMicros"}},"required":["missing","observed_at"],"type":"object"}},"required":["state","value"],"type":"object"},{"properties":{"state":{"const":"unavailable","type":"string"},"value":{"properties":{"observed_at":{"$ref":"#/$defs/UtcMicros"},"reason":{"$ref":"#/$defs/RemoteAuthorityUnavailableReasonV1"}},"required":["reason","observed_at"],"type":"object"}},"required":["state","value"],"type":"object"}]}, CurrentRemoteAuthorityV1: {"additionalProperties":false,"description":"Authenticated current authority for a mutable shard.","properties":{"credential_revision":{"description":"Revision of the authority node's current enrollment credential.","format":"uint64","minimum":0,"type":"integer"},"fence":{"$ref":"#/$defs/RemoteWriterFenceV1"},"observed_at":{"$ref":"#/$defs/UtcMicros"}},"required":["fence","credential_revision","observed_at"],"type":"object"}, + DaemonServiceUnitObservationV1: {"description":"The managed daemon service unit as a client observed it, from the unit\nfile alone.","oneOf":[{"additionalProperties":false,"properties":{"state":{"const":"not_installed","type":"string"}},"required":["state"],"type":"object"},{"additionalProperties":false,"description":"The unit file could not be read.","properties":{"error":{"type":"string"},"state":{"const":"unobservable","type":"string"}},"required":["state","error"],"type":"object"},{"additionalProperties":false,"description":"The unit file at `path` serves the socket `serves`.","properties":{"path":{"type":"string"},"serves":{"type":"string"},"state":{"const":"installed","type":"string"}},"required":["state","path","serves"],"type":"object"}]}, DashboardActionV1: {"description":"What `tracedecay_dashboard` does.","enum":["start","stop"],"type":"string"}, DeadCodeSymbolV1: {"additionalProperties":false,"properties":{"file":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string"},"line":{"format":"uint32","minimum":0,"type":"integer"},"name":{"type":"string"},"signature":{"type":["string","null"]}},"required":["id","name","kind","file","line"],"type":"object"}, Deadline: {"additionalProperties":false,"description":"One immutable deadline supplied by the caller or upstream admission layer.","properties":{"expires_at":{"$ref":"#/$defs/UtcMicros"}},"required":["expires_at"],"type":"object"}, @@ -3264,7 +3266,7 @@ const SCHEMAS: readonly CanonicalJsonSchema[] = [ {"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"authority_audit":{"default":false,"description":"Run the exhaustive observation-authority audit and include\nauthority_audit_ok (true = audit ran and passed, false = audit ran and\nfailed, null = audit did not run), authority_audit_reason (typed:\nauthority_invariant_failed, authority_store_unavailable,\nauthority_store_missing, authority_audit_not_run), and\nauthority_audit_error (observed detail) in database telemetry. Also\nincludes session-temporal health. Default false.","type":"boolean"},"doctor_report":{"default":false,"description":"Include the daemon-owned canonical Doctor report and typed per-table\ngrowth evidence. Default false.","type":"boolean"},"session_ingest_health":{"default":false,"description":"Include Cursor transcript-ingest health from the daemon-retained\nproject session authority. Default false.","type":"boolean"},"session_temporal_health":{"default":false,"description":"Include session-temporal health without the authority audit. Default\nfalse.","type":"boolean"},"startup_health":{"default":false,"description":"Ask a connection's first request for only the daemon-mounted database\nintegrity telemetry, for post-update startup validation; the daemon\ncore answers that probe before a project owner exists, and a project\nowner's snapshot already carries the same telemetry. Default false.","type":"boolean"}},"title":"RuntimeSurfaceRequestV1","type":"object"}, {"$defs":{"DoctorCoverageCompletenessV1":DEFINITIONS.DoctorCoverageCompletenessV1,"DoctorCoverageStatementV1":DEFINITIONS.DoctorCoverageStatementV1,"DoctorEvidenceRefV1":DEFINITIONS.DoctorEvidenceRefV1,"DoctorEvidenceReferenceV1":DEFINITIONS.DoctorEvidenceReferenceV1,"DoctorEvidenceStateV1":DEFINITIONS.DoctorEvidenceStateV1,"DoctorFamilyConsultationV1":DEFINITIONS.DoctorFamilyConsultationV1,"DoctorFamilyCoverageV1":DEFINITIONS.DoctorFamilyCoverageV1,"DoctorFamilyUnavailableReasonV1":DEFINITIONS.DoctorFamilyUnavailableReasonV1,"DoctorFindingFamilyV1":DEFINITIONS.DoctorFindingFamilyV1,"DoctorFindingV1":DEFINITIONS.DoctorFindingV1,"DoctorReportCoverageV1":DEFINITIONS.DoctorReportCoverageV1,"DoctorReportEntryV1":DEFINITIONS.DoctorReportEntryV1,"DoctorReportV1":DEFINITIONS.DoctorReportV1,"DoctorStorageFindingKindV1":DEFINITIONS.DoctorStorageFindingKindV1,"LanguageServerAnalyzerStateV1":DEFINITIONS.LanguageServerAnalyzerStateV1,"LanguageServerAnalyzerV1":DEFINITIONS.LanguageServerAnalyzerV1,"LanguageServerReadV1":DEFINITIONS.LanguageServerReadV1,"LanguageServerStateV1":DEFINITIONS.LanguageServerStateV1,"RuntimeDoctorReportV1":DEFINITIONS.RuntimeDoctorReportV1,"SchemaConvergenceFindingV1":DEFINITIONS.SchemaConvergenceFindingV1,"SchemaConvergenceProgressV1":DEFINITIONS.SchemaConvergenceProgressV1,"SchemaConvergenceStageV1":DEFINITIONS.SchemaConvergenceStageV1,"SchemaConvergenceStateV1":DEFINITIONS.SchemaConvergenceStateV1,"StorageByteSizeV1":DEFINITIONS.StorageByteSizeV1,"StoreKeyV1":DEFINITIONS.StoreKeyV1,"TableGrowthDoctorEvidenceV1":DEFINITIONS.TableGrowthDoctorEvidenceV1,"TableNameV1":DEFINITIONS.TableNameV1,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"description":"Process, database, and session-observation telemetry for the running\ndaemon, with the sections the request opted into.","properties":{"captured_at":{"description":"Unix epoch seconds.","format":"uint64","minimum":0,"type":"integer"},"cursor_session_ingest":true,"cursor_session_placeholder_paths":{"items":{"type":"string"},"type":["array","null"]},"database":{"description":"Store telemetry, with the authority-audit verdict when it ran."},"doctor_report":{"anyOf":[{"$ref":"#/$defs/RuntimeDoctorReportV1"},{"type":"null"}]},"host_os":{"type":"string"},"process":{"description":"Cached process sample."},"session_temporal_health":true,"tracedecay_version":{"type":"string"}},"required":["captured_at","tracedecay_version","host_os","process","database"],"title":"RuntimeResultV1","type":"object"}, {"$defs":{"SearchLexicalAliasV1":DEFINITIONS.SearchLexicalAliasV1,"SearchLexicalFieldFilterV1":DEFINITIONS.SearchLexicalFieldFilterV1,"SearchLexicalFieldV1":DEFINITIONS.SearchLexicalFieldV1,"SearchLexicalProximityV1":DEFINITIONS.SearchLexicalProximityV1},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"cursor":{"description":"Authenticated opaque continuation returned as next_cursor. Repeat the same query and lexical options with it.","type":["string","null"]},"lazy_index_ignored_dependencies":{"description":"Opt in to bounded indexing of ignored dependency entry files when an import hint matches (default: false).","type":["boolean","null"]},"lexical_aliases":{"description":"Named query-time vocabulary aliases. The strict query always ranks first. Alias-only hits follow it with the strict query, alternative, and configured-vocabulary reason disclosed.","items":{"$ref":"#/$defs/SearchLexicalAliasV1"},"maxItems":8,"type":["array","null"]},"lexical_anchors":{"description":"Exact identifiers or technical terms (e.g. 'reserve_stock', 'Foo::bar', 'E0308') that the answer must be about. Each is ranked through the lexical lane as its own route: a hit carrying an anchor outranks every hit that carries none, exact hits included, every anchor with matches keeps at least its best sites through the lane cap, and `lexical_anchors` in the response reports each anchor's outcome (`matched` rows, `admitted` sites this response returns, `dropped` admitted sites it could not carry with the reason, `unmatched`, or `not_served`). Ranked retrieval, not exhaustive grep (use tracedecay_grep for that). Each result names the routes that ranked it. At most 8 anchors, each one whitespace-free term of at most 128 bytes, no repeats.","items":{"type":"string"},"maxItems":8,"type":["array","null"]},"lexical_field_filters":{"description":"Include or exclude lexical symbol_name, qualified_name, path, signature, documentation, body_text, preamble_text, exact_term, or subtoken fields.","items":{"$ref":"#/$defs/SearchLexicalFieldFilterV1"},"maxItems":9,"type":["array","null"]},"lexical_phrases":{"description":"Exact lexical phrases to rank through n-gram candidate pruning.","items":{"type":"string"},"maxItems":4,"type":["array","null"]},"lexical_proximities":{"description":"Ordered lexical terms that must occur within the bounded intervening-token gap.","items":{"$ref":"#/$defs/SearchLexicalProximityV1"},"maxItems":4,"type":["array","null"]},"limit":{"description":"Maximum number of results to return (default: 10)","format":"uint64","minimum":0,"type":["integer","null"]},"prefer_symbol":{"description":"Add a lexical route restricted to symbol-name matches for the identifier-shaped words of the query (default: false). Query words such as class/struct/function/find/explain are ignored; 'Foo::bar' and 'Foo.bar' contribute 'bar'.","type":["boolean","null"]},"query":{"description":"Search query string to match against symbol names","type":"string"}},"required":["query"],"title":"SearchSurfaceRequestV1","type":"object"}, - {"$defs":{"ApplicationProblemDetailV1":DEFINITIONS.ApplicationProblemDetailV1_2,"CalibrationProfileId":DEFINITIONS.CalibrationProfileId,"CandidateContribution":DEFINITIONS.CandidateContribution,"CodeIndexBuildBlockedReasonV1":DEFINITIONS.CodeIndexBuildBlockedReasonV1,"CodeIndexConvergenceParkedV1":DEFINITIONS.CodeIndexConvergenceParkedV1,"CodeIndexStalenessStateV1":DEFINITIONS.CodeIndexStalenessStateV1,"ComponentRevision":DEFINITIONS.ComponentRevision,"ContextLexicalAnchorV1":DEFINITIONS.ContextLexicalAnchorV1,"DiagnosticsSearchedTsconfigV1":DEFINITIONS.DiagnosticsSearchedTsconfigV1,"EvidenceRole":DEFINITIONS.EvidenceRole,"ExactClass":DEFINITIONS.ExactClass,"FileOccurrenceId":DEFINITIONS.FileOccurrenceId,"FixedPointScore":DEFINITIONS.FixedPointScore,"FreshnessCompatibilityV1":DEFINITIONS.FreshnessCompatibilityV1,"FusedCandidate":DEFINITIONS.FusedCandidate,"LexicalAnchorDropReasonV1":DEFINITIONS.LexicalAnchorDropReasonV1,"LexicalAnchorDropV1":DEFINITIONS.LexicalAnchorDropV1,"LogicalCopyClusterId":DEFINITIONS.LogicalCopyClusterId,"LogicalEvidenceId":DEFINITIONS.LogicalEvidenceId,"OccurrenceProvenance":DEFINITIONS.OccurrenceProvenance,"PrimitiveFreshnessStateV1":DEFINITIONS.PrimitiveFreshnessStateV1,"PrimitiveIndexingStateV1":DEFINITIONS.PrimitiveIndexingStateV1,"PrimitiveLaneCompleteV1":DEFINITIONS.PrimitiveLaneCompleteV1,"PrimitiveRecallV1":DEFINITIONS.PrimitiveRecallV1,"PrimitiveSearchFreshnessV1":DEFINITIONS.PrimitiveSearchFreshnessV1,"PrimitiveUnavailableEvidenceV1":DEFINITIONS.PrimitiveUnavailableEvidenceV1,"PrimitiveUnavailableStatusV1":DEFINITIONS.PrimitiveUnavailableStatusV1,"RankingDecision":DEFINITIONS.RankingDecision,"RankingDecisionKind":DEFINITIONS.RankingDecisionKind,"RepositoryId":DEFINITIONS.RepositoryId,"RetrievalAnchorId":DEFINITIONS.RetrievalAnchorId,"RetrieverKind":DEFINITIONS.RetrieverKind,"ScoreDomainId":DEFINITIONS.ScoreDomainId,"SearchCompleteV1":DEFINITIONS.SearchCompleteV1,"SearchCoverageV1":DEFINITIONS.SearchCoverageV1,"SearchDisplayUnavailableV1":DEFINITIONS.SearchDisplayUnavailableV1,"SearchExternalImportCandidatesV1":DEFINITIONS.SearchExternalImportCandidatesV1,"SearchExternalImportHintV1":DEFINITIONS.SearchExternalImportHintV1,"SearchExternalImportV1":DEFINITIONS.SearchExternalImportV1,"SearchLaneStateV1":DEFINITIONS.SearchLaneStateV1,"SearchLaneStatusV1":DEFINITIONS.SearchLaneStatusV1,"SearchLexicalAlternativeReasonV1":DEFINITIONS.SearchLexicalAlternativeReasonV1,"SearchLexicalRouteV1":DEFINITIONS.SearchLexicalRouteV1,"SearchResultDisplayV1":DEFINITIONS.SearchResultDisplayV1,"SearchResultRowV1":DEFINITIONS.SearchResultRowV1,"SearchRouteMatchV1":DEFINITIONS.SearchRouteMatchV1,"SearchSpellingVariantV1":DEFINITIONS.SearchSpellingVariantV1,"SearchUnavailableV1":DEFINITIONS.SearchUnavailableV1,"SessionOrThreadId":DEFINITIONS.SessionOrThreadId,"SourceFreshness":DEFINITIONS.SourceFreshness,"SourceInstanceKey":DEFINITIONS.SourceInstanceKey,"SourceNamespace":DEFINITIONS.SourceNamespace,"SourceOccurrenceId":DEFINITIONS.SourceOccurrenceId,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","anyOf":[{"$ref":"#/$defs/SearchCompleteV1"},{"$ref":"#/$defs/SearchUnavailableV1"}],"description":"What a search served: a ranked page, or the typed reason no generation\ncould answer.","title":"SearchResultV1"}, + {"$defs":{"ApplicationProblemDetailV1":DEFINITIONS.ApplicationProblemDetailV1_2,"CalibrationProfileId":DEFINITIONS.CalibrationProfileId,"CandidateContribution":DEFINITIONS.CandidateContribution,"CodeIndexBuildBlockedReasonV1":DEFINITIONS.CodeIndexBuildBlockedReasonV1,"CodeIndexConvergenceParkedV1":DEFINITIONS.CodeIndexConvergenceParkedV1,"CodeIndexStalenessStateV1":DEFINITIONS.CodeIndexStalenessStateV1,"ComponentRevision":DEFINITIONS.ComponentRevision,"ContextLexicalAnchorV1":DEFINITIONS.ContextLexicalAnchorV1,"DaemonServiceUnitObservationV1":DEFINITIONS.DaemonServiceUnitObservationV1,"DiagnosticsSearchedTsconfigV1":DEFINITIONS.DiagnosticsSearchedTsconfigV1,"EvidenceRole":DEFINITIONS.EvidenceRole,"ExactClass":DEFINITIONS.ExactClass,"FileOccurrenceId":DEFINITIONS.FileOccurrenceId,"FixedPointScore":DEFINITIONS.FixedPointScore,"FreshnessCompatibilityV1":DEFINITIONS.FreshnessCompatibilityV1,"FusedCandidate":DEFINITIONS.FusedCandidate,"LexicalAnchorDropReasonV1":DEFINITIONS.LexicalAnchorDropReasonV1,"LexicalAnchorDropV1":DEFINITIONS.LexicalAnchorDropV1,"LogicalCopyClusterId":DEFINITIONS.LogicalCopyClusterId,"LogicalEvidenceId":DEFINITIONS.LogicalEvidenceId,"OccurrenceProvenance":DEFINITIONS.OccurrenceProvenance,"PrimitiveFreshnessStateV1":DEFINITIONS.PrimitiveFreshnessStateV1,"PrimitiveIndexingStateV1":DEFINITIONS.PrimitiveIndexingStateV1,"PrimitiveLaneCompleteV1":DEFINITIONS.PrimitiveLaneCompleteV1,"PrimitiveRecallV1":DEFINITIONS.PrimitiveRecallV1,"PrimitiveSearchFreshnessV1":DEFINITIONS.PrimitiveSearchFreshnessV1,"PrimitiveUnavailableEvidenceV1":DEFINITIONS.PrimitiveUnavailableEvidenceV1,"PrimitiveUnavailableStatusV1":DEFINITIONS.PrimitiveUnavailableStatusV1,"RankingDecision":DEFINITIONS.RankingDecision,"RankingDecisionKind":DEFINITIONS.RankingDecisionKind,"RepositoryId":DEFINITIONS.RepositoryId,"RetrievalAnchorId":DEFINITIONS.RetrievalAnchorId,"RetrieverKind":DEFINITIONS.RetrieverKind,"ScoreDomainId":DEFINITIONS.ScoreDomainId,"SearchCompleteV1":DEFINITIONS.SearchCompleteV1,"SearchCoverageV1":DEFINITIONS.SearchCoverageV1,"SearchDisplayUnavailableV1":DEFINITIONS.SearchDisplayUnavailableV1,"SearchExternalImportCandidatesV1":DEFINITIONS.SearchExternalImportCandidatesV1,"SearchExternalImportHintV1":DEFINITIONS.SearchExternalImportHintV1,"SearchExternalImportV1":DEFINITIONS.SearchExternalImportV1,"SearchLaneStateV1":DEFINITIONS.SearchLaneStateV1,"SearchLaneStatusV1":DEFINITIONS.SearchLaneStatusV1,"SearchLexicalAlternativeReasonV1":DEFINITIONS.SearchLexicalAlternativeReasonV1,"SearchLexicalRouteV1":DEFINITIONS.SearchLexicalRouteV1,"SearchResultDisplayV1":DEFINITIONS.SearchResultDisplayV1,"SearchResultRowV1":DEFINITIONS.SearchResultRowV1,"SearchRouteMatchV1":DEFINITIONS.SearchRouteMatchV1,"SearchSpellingVariantV1":DEFINITIONS.SearchSpellingVariantV1,"SearchUnavailableV1":DEFINITIONS.SearchUnavailableV1,"SessionOrThreadId":DEFINITIONS.SessionOrThreadId,"SourceFreshness":DEFINITIONS.SourceFreshness,"SourceInstanceKey":DEFINITIONS.SourceInstanceKey,"SourceNamespace":DEFINITIONS.SourceNamespace,"SourceOccurrenceId":DEFINITIONS.SourceOccurrenceId,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","anyOf":[{"$ref":"#/$defs/SearchCompleteV1"},{"$ref":"#/$defs/SearchUnavailableV1"}],"description":"What a search served: a ranked page, or the typed reason no generation\ncould answer.","title":"SearchResultV1"}, {"$defs":{"PageRequest":DEFINITIONS.PageRequest,"ResultProjection":DEFINITIONS.ResultProjection,"RetrievalOrder":DEFINITIONS.RetrievalOrder,"RetrievalRequestMeta":DEFINITIONS.RetrievalRequestMeta,"SessionId":DEFINITIONS.SessionId,"TemporalModeV1":DEFINITIONS.TemporalModeV1,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"meta":{"$ref":"#/$defs/RetrievalRequestMeta"},"session_id":{"$ref":"#/$defs/SessionId"}},"required":["session_id","meta"],"title":"SessionLookupRequest","type":"object"}, {"$defs":{"RetrievalAnchorId":DEFINITIONS.RetrievalAnchorId},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"anchors":{"items":{"$ref":"#/$defs/RetrievalAnchorId"},"type":"array"}},"required":["anchors"],"title":"SessionLookupResult","type":"object"}, {"$defs":{"SessionRefreshFrontierV1":DEFINITIONS.SessionRefreshFrontierV1,"SessionRefreshGrainV1":DEFINITIONS.SessionRefreshGrainV1,"SessionRefreshScopeV1":DEFINITIONS.SessionRefreshScopeV1,"SessionRefreshSessionV1":DEFINITIONS.SessionRefreshSessionV1,"SessionRefreshSourceV1":DEFINITIONS.SessionRefreshSourceV1,"SessionRefreshTargetV1":DEFINITIONS.SessionRefreshTargetV1,"TemporalModeV1":DEFINITIONS.TemporalModeV1,"UtcMicros":DEFINITIONS.UtcMicros},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"description":"Exact route-selected session-refresh request body.\n\nEach current route selects the action itself; `scope` selects the mounted\nsession-store owner. Project-scoped requests are served under project-open\nadmission, profile-scoped requests by the authenticated profile authority.","properties":{"handle":{"type":["string","null"]},"scope":{"$ref":"#/$defs/SessionRefreshScopeV1"},"session":{"$ref":"#/$defs/SessionRefreshSessionV1"},"source":{"$ref":"#/$defs/SessionRefreshSourceV1"},"target":{"$ref":"#/$defs/SessionRefreshTargetV1"}},"required":["scope","session","source","target"],"title":"SessionRefreshActionRequestV1","type":"object"},