diff options
Diffstat (limited to 'crates/phone-opus/src/mcp')
| -rw-r--r-- | crates/phone-opus/src/mcp/catalog.rs | 6 | ||||
| -rw-r--r-- | crates/phone-opus/src/mcp/fault.rs | 27 | ||||
| -rw-r--r-- | crates/phone-opus/src/mcp/service.rs | 183 |
3 files changed, 64 insertions, 152 deletions
diff --git a/crates/phone-opus/src/mcp/catalog.rs b/crates/phone-opus/src/mcp/catalog.rs index f17a3c5..cf18fc3 100644 --- a/crates/phone-opus/src/mcp/catalog.rs +++ b/crates/phone-opus/src/mcp/catalog.rs @@ -41,7 +41,7 @@ impl ToolSpec { const TOOL_SPECS: &[ToolSpec] = &[ ToolSpec { name: "consult", - description: "Run a blocking consult against the system Claude Code install using a read-only built-in toolset, automatically reuse the remembered context for the current cwd by default, optionally opt out with fresh_context, and return the response plus execution metadata.", + description: "Run a blocking one-shot consult against the system Claude Code install using a read-only built-in toolset and return the response plus execution metadata.", dispatch: DispatchTarget::Worker, replay: ReplayContract::NeverReplay, }, @@ -89,10 +89,6 @@ fn tool_schema(name: &str) -> Value { "cwd": { "type": "string", "description": "Optional working directory for the Claude Code session. Relative paths resolve against the MCP host working directory." - }, - "fresh_context": { - "type": "boolean", - "description": "When true, start a fresh Claude context instead of reusing the remembered context for this cwd. Defaults to false." } }, "required": ["prompt"] diff --git a/crates/phone-opus/src/mcp/fault.rs b/crates/phone-opus/src/mcp/fault.rs index c2d4a6c..b0b1e28 100644 --- a/crates/phone-opus/src/mcp/fault.rs +++ b/crates/phone-opus/src/mcp/fault.rs @@ -13,14 +13,6 @@ pub(crate) struct FaultContext { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub(crate) struct ConsultFaultContext { pub(crate) cwd: String, - pub(crate) context_mode: String, - pub(crate) planned_session_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) reused_session_id: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) observed_session_id: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) resume_session_id: Option<String>, #[serde(default, skip_serializing_if = "is_false")] pub(crate) quota_limited: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -200,23 +192,8 @@ impl FaultRecord { else { return lines.join("\n"); }; - let mut fields: BTreeMap<String, String> = BTreeMap::from([ - ("cwd".to_owned(), consult.cwd.clone()), - ("context_mode".to_owned(), consult.context_mode.clone()), - ( - "planned_session".to_owned(), - consult.planned_session_id.clone(), - ), - ]); - if let Some(session_id) = consult.reused_session_id.as_ref() { - let _ = fields.insert("reused_session".to_owned(), session_id.clone()); - } - if let Some(session_id) = consult.observed_session_id.as_ref() { - let _ = fields.insert("observed_session".to_owned(), session_id.clone()); - } - if let Some(session_id) = consult.resume_session_id.as_ref() { - let _ = fields.insert("resume_session".to_owned(), session_id.clone()); - } + let mut fields: BTreeMap<String, String> = + BTreeMap::from([("cwd".to_owned(), consult.cwd.clone())]); if consult.quota_limited { let _ = fields.insert("quota_limited".to_owned(), "true".to_owned()); } diff --git a/crates/phone-opus/src/mcp/service.rs b/crates/phone-opus/src/mcp/service.rs index bb56ad3..e152fa7 100644 --- a/crates/phone-opus/src/mcp/service.rs +++ b/crates/phone-opus/src/mcp/service.rs @@ -156,14 +156,7 @@ impl ConsultRequest { let cwd = WorkingDirectory::resolve(args.cwd)?; let context_key = ConsultContextKey::from_cwd(&cwd); let fresh_context = args.fresh_context.unwrap_or(false); - let session_plan = if fresh_context { - ConsultSessionPlan::fresh() - } else { - load_consult_context(&context_key) - .map_err(|source| ConsultRequestError::ContextIndex { source })? - .and_then(ConsultSessionPlan::from_stored) - .unwrap_or_else(ConsultSessionPlan::fresh) - }; + let session_plan = ConsultSessionPlan::fresh(); Ok(Self { prompt, cwd, @@ -173,6 +166,10 @@ impl ConsultRequest { }) } + fn planned_session_id(&self) -> String { + self.session_plan.planned_session().display() + } + fn context_mode(&self) -> &'static str { self.session_plan.context_mode() } @@ -181,16 +178,6 @@ impl ConsultRequest { self.session_plan.reused_session_id() } - fn planned_session_id(&self) -> String { - self.session_plan.planned_session().display() - } - - fn launch_resume_session(&self) -> Option<String> { - self.session_plan - .resume_session() - .map(SessionHandle::display) - } - fn launch_session_id(&self) -> Option<String> { match self.session_plan { ConsultSessionPlan::Start { .. } => Some(self.planned_session_id()), @@ -214,14 +201,6 @@ impl ConsultRequest { } } - fn current_context_session_id(&self) -> Option<String> { - load_consult_context(&self.context_key) - .ok() - .flatten() - .and_then(ConsultSessionPlan::from_stored) - .map(|plan| plan.planned_session().display()) - } - #[allow(dead_code, reason = "background submission is parked but not exposed")] fn background_request(&self) -> BackgroundConsultRequest { BackgroundConsultRequest { @@ -351,6 +330,10 @@ struct ConsultContextIndex { } impl ConsultContextIndex { + #[allow( + dead_code, + reason = "context lookup is parked while session reuse stays disabled" + )] fn context_for(&self, key: &ConsultContextKey) -> Option<StoredConsultContext> { self.by_cwd.get(key.as_str()).cloned() } @@ -378,6 +361,10 @@ enum ConsultSessionPlan { session: SessionHandle, reused: bool, }, + #[allow( + dead_code, + reason = "resume plans are parked while one-shot consults are enforced" + )] Resume(SessionHandle), } @@ -389,6 +376,10 @@ impl ConsultSessionPlan { } } + #[allow( + dead_code, + reason = "stored-session revival is parked while one-shot consults are enforced" + )] fn from_stored(context: StoredConsultContext) -> Option<Self> { let session = SessionHandle::parse(context.session_id.as_str())?; Some(match context.state { @@ -406,6 +397,10 @@ impl ConsultSessionPlan { } } + #[allow( + dead_code, + reason = "resume paths are parked while one-shot consults are enforced" + )] fn resume_session(&self) -> Option<&SessionHandle> { match self { Self::Resume(session) => Some(session), @@ -647,6 +642,10 @@ enum ConsultRequestError { Canonicalize { path: String, source: io::Error }, #[error("working directory `{0}` is not a directory")] NotDirectory(String), + #[allow( + dead_code, + reason = "context index loading is parked while one-shot consults are enforced" + )] #[error("failed to resolve consult context state: {source}")] ContextIndex { source: io::Error }, #[error("job_id must be a valid UUID, got `{0}`")] @@ -705,14 +704,30 @@ struct ConsultResponse { cwd: WorkingDirectory, result: String, persisted_output_path: PersistedConsultPath, + #[allow( + dead_code, + reason = "session metadata is retained internally but hidden from the public surface" + )] context_mode: &'static str, + #[allow( + dead_code, + reason = "session metadata is retained internally but hidden from the public surface" + )] planned_session_id: String, + #[allow( + dead_code, + reason = "session metadata is retained internally but hidden from the public surface" + )] reused_session_id: Option<String>, duration_ms: u64, duration_api_ms: Option<u64>, num_turns: u64, stop_reason: Option<String>, session_id: Option<String>, + #[allow( + dead_code, + reason = "session metadata is retained internally but hidden from the public surface" + )] observed_session_id: Option<String>, total_cost_usd: Option<f64>, usage: Option<Value>, @@ -932,18 +947,14 @@ impl ClaudeSandbox { struct PersistedConsultPath(PathBuf); impl PersistedConsultPath { - fn new(request: &ConsultRequest, session_id: Option<&str>) -> io::Result<Self> { + fn new(request: &ConsultRequest) -> io::Result<Self> { fs::create_dir_all(CONSULT_OUTPUT_ROOT)?; let timestamp = OffsetDateTime::now_utc() .format(CONSULT_TIMESTAMP_FORMAT) .map_err(|error| io::Error::other(error.to_string()))?; let slug = consult_slug(request.prompt.as_str()); - let session_slug = session_id.map_or_else( - || "session-none".to_owned(), - |session_id| format!("session-{}", consult_slug(session_id)), - ); Ok(Self(Path::new(CONSULT_OUTPUT_ROOT).join(format!( - "{timestamp}-{slug}-{session_slug}-{}.json", + "{timestamp}-{slug}-{}.json", Uuid::new_v4() )))) } @@ -1010,27 +1021,12 @@ fn consult_fault_context(request: &ConsultRequest, error: &ConsultInvocationErro | ConsultInvocationError::Stalled(detail) | ConsultInvocationError::Downstream(detail) => Some(detail.as_str()), }; - let reused_session_id = request.reused_session_id(); - let planned_session_id = request.planned_session_id(); - let observed_session_id = detail - .and_then(downstream_session_id) - .clone() - .or_else(|| request.current_context_session_id()); - let resume_session_id = observed_session_id - .clone() - .or_else(|| reused_session_id.clone()) - .or_else(|| Some(planned_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()); + let retry_hint = consult_retry_hint(quota_limited, error); FaultContext { consult: Some(ConsultFaultContext { cwd: request.cwd.display(), - context_mode: request.context_mode().to_owned(), - planned_session_id, - reused_session_id, - observed_session_id, - resume_session_id, quota_limited, quota_reset_hint, retry_hint, @@ -1038,34 +1034,23 @@ fn consult_fault_context(request: &ConsultRequest, error: &ConsultInvocationErro } } -fn downstream_session_id(detail: &str) -> Option<String> { - let value = serde_json::from_str::<Value>(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<String> { 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<String> { +fn consult_retry_hint(quota_limited: bool, error: &ConsultInvocationError) -> Option<String> { 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() - } - }); + return Some("wait for the quota window to reset, then retry the consult".to_owned()); + } + match error { + ConsultInvocationError::Stalled(_) => Some( + "Claude stalled before producing output; retry the consult as a fresh one-shot call" + .to_owned(), + ), + _ => None, } - 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( @@ -1502,9 +1487,6 @@ fn invoke_claude(request: &ConsultRequest) -> Result<ConsultResponse, ConsultInv if let Some(session_id) = request.launch_session_id() { let _ = command.arg("--session-id").arg(session_id); } - if let Some(session_id) = request.launch_resume_session() { - let _ = command.arg("--resume").arg(session_id); - } let mut child = command .arg(request.prompt.rendered()) .spawn() @@ -1577,14 +1559,8 @@ fn invoke_claude(request: &ConsultRequest) -> Result<ConsultResponse, ConsultInv request .remember_context(canonical_session_id.as_deref()) .map_err(ConsultInvocationError::Spawn)?; - let persisted_output_path = persist_consult_output( - request, - &result, - &envelope, - canonical_session_id.as_deref(), - observed_session_id.as_deref(), - ) - .map_err(ConsultInvocationError::Spawn)?; + let persisted_output_path = persist_consult_output(request, &result, &envelope) + .map_err(ConsultInvocationError::Spawn)?; Ok(ConsultResponse { cwd: request.cwd.clone(), result, @@ -1796,6 +1772,10 @@ fn load_consult_context_index() -> io::Result<ConsultContextIndex> { } } +#[allow( + dead_code, + reason = "context lookup is parked while one-shot consults are enforced" +)] fn load_consult_context(key: &ConsultContextKey) -> io::Result<Option<StoredConsultContext>> { Ok(load_consult_context_index()?.context_for(key)) } @@ -1886,10 +1866,8 @@ fn persist_consult_output( request: &ConsultRequest, result: &str, envelope: &ClaudeJsonEnvelope, - session_id: Option<&str>, - observed_session_id: Option<&str>, ) -> io::Result<PersistedConsultPath> { - let path = PersistedConsultPath::new(request, session_id)?; + let path = PersistedConsultPath::new(request)?; let saved_at = OffsetDateTime::now_utc() .format(&Rfc3339) .map_err(|error| io::Error::other(error.to_string()))?; @@ -1901,17 +1879,12 @@ fn persist_consult_output( "prompt": request.prompt.as_str(), "prompt_prefix": CLAUDE_CONSULT_PREFIX, "effective_prompt": request.prompt.rendered(), - "context_mode": request.context_mode(), - "planned_session_id": request.planned_session_id(), - "reused_session_id": request.reused_session_id(), "response": result, "model": model_name(envelope.model_usage.as_ref()), "duration_ms": envelope.duration_ms.unwrap_or(0), "duration_api_ms": envelope.duration_api_ms, "num_turns": envelope.num_turns.unwrap_or(0), "stop_reason": envelope.stop_reason, - "session_id": session_id, - "observed_session_id": observed_session_id, "total_cost_usd": envelope.total_cost_usd, "usage": envelope.usage, "model_usage": envelope.model_usage, @@ -2002,16 +1975,11 @@ fn consult_output( "response": response.result, "cwd": response.cwd.display(), "persisted_output_path": response.persisted_output_path.display(), - "context_mode": response.context_mode, - "planned_session_id": response.planned_session_id, - "reused_session_id": response.reused_session_id, "prompt_prefix_injected": true, "model": response.model_name(), "duration_ms": response.duration_ms, "num_turns": response.num_turns, "stop_reason": response.stop_reason, - "session_id": response.session_id, - "observed_session_id": response.observed_session_id, "total_cost_usd": response.total_cost_usd, "permission_denial_count": response.permission_denials.len(), }); @@ -2022,15 +1990,10 @@ fn consult_output( "prompt": request.prompt.as_str(), "prompt_prefix": CLAUDE_CONSULT_PREFIX, "effective_prompt": request.prompt.rendered(), - "context_mode": response.context_mode, - "planned_session_id": response.planned_session_id, - "reused_session_id": response.reused_session_id, "duration_ms": response.duration_ms, "duration_api_ms": response.duration_api_ms, "num_turns": response.num_turns, "stop_reason": response.stop_reason, - "session_id": response.session_id, - "observed_session_id": response.observed_session_id, "total_cost_usd": response.total_cost_usd, "usage": response.usage, "model_usage": response.model_usage, @@ -2053,7 +2016,6 @@ fn consult_output( fn concise_text(_request: &ConsultRequest, response: &ConsultResponse) -> String { let mut status = vec![ "consult ok".to_owned(), - format!("context={}", response.context_mode), format!("turns={}", response.num_turns), format!("duration={}", render_duration_ms(response.duration_ms)), ]; @@ -2069,16 +2031,6 @@ fn concise_text(_request: &ConsultRequest, response: &ConsultResponse) -> String let mut lines = vec![status.join(" ")]; lines.push(format!("cwd: {}", response.cwd.display())); - lines.push(format!("planned_session: {}", response.planned_session_id)); - if let Some(session_id) = response.reused_session_id.as_deref() { - lines.push(format!("reused_session: {session_id}")); - } - if let Some(session_id) = response.observed_session_id.as_deref() { - lines.push(format!("observed_session: {session_id}")); - } - if let Some(session_id) = response.session_id.as_deref() { - lines.push(format!("session: {session_id}")); - } lines.push(format!( "saved: {}", response.persisted_output_path.display() @@ -2096,20 +2048,10 @@ fn concise_text(_request: &ConsultRequest, response: &ConsultResponse) -> String fn full_text(_request: &ConsultRequest, response: &ConsultResponse) -> String { let mut lines = vec![ - format!( - "consult ok context={} turns={}", - response.context_mode, response.num_turns - ), + format!("consult ok turns={}", response.num_turns), format!("cwd: {}", response.cwd.display()), - format!("planned_session: {}", response.planned_session_id), format!("duration: {}", render_duration_ms(response.duration_ms)), ]; - if let Some(session_id) = response.reused_session_id.as_deref() { - lines.push(format!("reused_session: {session_id}")); - } - if let Some(session_id) = response.observed_session_id.as_deref() { - lines.push(format!("observed_session: {session_id}")); - } if let Some(duration_api_ms) = response.duration_api_ms { lines.push(format!( "api_duration: {}", @@ -2122,9 +2064,6 @@ fn full_text(_request: &ConsultRequest, response: &ConsultResponse) -> String { if let Some(stop_reason) = response.stop_reason.as_deref() { lines.push(format!("stop: {stop_reason}")); } - if let Some(session_id) = response.session_id.as_deref() { - lines.push(format!("session: {session_id}")); - } lines.push(format!( "saved: {}", response.persisted_output_path.display() |