From 8b090c3d0daf8b336aab9074b0d8aa31a688e232 Mon Sep 17 00:00:00 2001 From: main Date: Tue, 24 Mar 2026 19:09:28 -0400 Subject: Surface reusable consult context on failures --- crates/phone-opus/src/mcp/service.rs | 67 ++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) (limited to 'crates/phone-opus/src/mcp/service.rs') diff --git a/crates/phone-opus/src/mcp/service.rs b/crates/phone-opus/src/mcp/service.rs index 993a0e4..c5c2d66 100644 --- a/crates/phone-opus/src/mcp/service.rs +++ b/crates/phone-opus/src/mcp/service.rs @@ -16,7 +16,7 @@ use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use users::get_current_uid; use uuid::Uuid; -use crate::mcp::fault::{FaultRecord, FaultStage}; +use crate::mcp::fault::{ConsultFaultContext, FaultContext, FaultRecord, FaultStage}; use crate::mcp::output::{ ToolOutput, fallback_detailed_tool_output, split_presentation, tool_success, }; @@ -89,7 +89,7 @@ impl WorkerService { let request = ConsultRequest::parse(args) .map_err(|error| invalid_consult_request(self.generation, &operation, error))?; let response = invoke_claude(&request) - .map_err(|error| consult_fault(self.generation, &operation, error))?; + .map_err(|error| consult_fault(self.generation, &operation, &request, error))?; consult_output(&request, &response, self.generation, &operation)? } other => { @@ -844,9 +844,11 @@ fn invalid_consult_request( fn consult_fault( generation: Generation, operation: &str, + request: &ConsultRequest, error: ConsultInvocationError, ) -> FaultRecord { - match error { + let context = consult_fault_context(request, &error); + let record = match error { ConsultInvocationError::Spawn(source) => FaultRecord::process( generation, FaultStage::Claude, @@ -857,7 +859,66 @@ fn consult_fault( | ConsultInvocationError::Downstream(detail) => { FaultRecord::downstream(generation, FaultStage::Claude, operation, detail) } + }; + record.with_context(context) +} + +fn consult_fault_context(request: &ConsultRequest, error: &ConsultInvocationError) -> FaultContext { + let detail = match error { + ConsultInvocationError::Spawn(_) => None, + ConsultInvocationError::InvalidJson(detail) + | ConsultInvocationError::Downstream(detail) => Some(detail.as_str()), + }; + let reused_session_id = request.reused_session_id(); + let downstream_session_id = detail.and_then(downstream_session_id); + let resume_session_id = downstream_session_id + .clone() + .or_else(|| reused_session_id.clone()); + let quota_reset_hint = detail.and_then(quota_reset_hint); + let quota_limited = quota_reset_hint.is_some(); + let retry_hint = consult_retry_hint(quota_limited, resume_session_id.as_deref()); + FaultContext { + consult: Some(ConsultFaultContext { + cwd: request.cwd.display(), + context_mode: request.context_mode().to_owned(), + reused_session_id, + downstream_session_id, + resume_session_id, + quota_limited, + quota_reset_hint, + retry_hint, + }), + } +} + +fn downstream_session_id(detail: &str) -> Option { + let value = serde_json::from_str::(detail).ok()?; + let session_id = value.get("session_id")?.as_str()?; + SessionHandle::parse(session_id).map(|session| session.display()) +} + +fn quota_reset_hint(detail: &str) -> Option { + let (_, suffix) = detail.split_once("resets ")?; + let hint = suffix.trim(); + (!hint.is_empty()).then(|| hint.to_owned()) +} + +fn consult_retry_hint(quota_limited: bool, resume_session_id: Option<&str>) -> Option { + if quota_limited { + return Some(match resume_session_id { + Some(session_id) => format!( + "wait for the quota window to reset, then retry consult on the same cwd; phone_opus will reuse resume_session {session_id} automatically" + ), + None => { + "wait for the quota window to reset, then retry consult on the same cwd".to_owned() + } + }); } + resume_session_id.map(|session_id| { + format!( + "retry consult on the same cwd; phone_opus will reuse resume_session {session_id} automatically" + ) + }) } pub(crate) fn consult_job_tool_output( -- cgit v1.2.3