From ff76c72f3c78694eebe4824318e85c4751343cf4 Mon Sep 17 00:00:00 2001 From: main Date: Mon, 23 Mar 2026 15:38:00 -0400 Subject: Support resuming Claude consult sessions --- crates/phone-opus/src/mcp/catalog.rs | 6 +++- crates/phone-opus/src/mcp/service.rs | 61 +++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 6 deletions(-) (limited to 'crates/phone-opus/src/mcp') diff --git a/crates/phone-opus/src/mcp/catalog.rs b/crates/phone-opus/src/mcp/catalog.rs index a7e7cf6..4c71e83 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 and return the response.", + description: "Run a blocking consult against the system Claude Code install using a read-only built-in toolset, optionally resume a prior Claude session by session_id, and return the response.", dispatch: DispatchTarget::Worker, replay: ReplayContract::NeverReplay, }, @@ -94,6 +94,10 @@ fn tool_schema(name: &str) -> Value { "type": "integer", "minimum": 1, "description": "Optional maximum number of Claude agent turns before stopping." + }, + "session_id": { + "type": "string", + "description": "Optional Claude session handle returned by a previous consult call. When set, phone_opus resumes that conversation instead of starting a fresh one." } }, "required": ["prompt"] diff --git a/crates/phone-opus/src/mcp/service.rs b/crates/phone-opus/src/mcp/service.rs index 2472887..c57d0db 100644 --- a/crates/phone-opus/src/mcp/service.rs +++ b/crates/phone-opus/src/mcp/service.rs @@ -7,6 +7,7 @@ use libmcp::{Generation, SurfaceKind}; use serde::Deserialize; use serde_json::{Value, json}; use thiserror::Error; +use uuid::Uuid; use crate::mcp::fault::{FaultRecord, FaultStage}; use crate::mcp::output::{ @@ -100,6 +101,7 @@ struct ConsultArgs { prompt: String, cwd: Option, max_turns: Option, + session_id: Option, } #[derive(Debug, Clone)] @@ -107,6 +109,7 @@ struct ConsultRequest { prompt: PromptText, cwd: WorkingDirectory, max_turns: Option, + session: Option, } impl ConsultRequest { @@ -115,8 +118,21 @@ impl ConsultRequest { prompt: PromptText::parse(args.prompt)?, cwd: WorkingDirectory::resolve(args.cwd)?, max_turns: args.max_turns.map(TurnLimit::parse).transpose()?, + session: args.session_id.map(SessionHandle::parse).transpose()?, }) } + + fn session_mode(&self) -> &'static str { + if self.session.is_some() { + "resumed" + } else { + "new" + } + } + + fn requested_session_id(&self) -> Option { + self.session.as_ref().map(SessionHandle::display) + } } #[derive(Debug, Clone)] @@ -188,6 +204,21 @@ impl TurnLimit { } } +#[derive(Debug, Clone)] +struct SessionHandle(Uuid); + +impl SessionHandle { + fn parse(raw: String) -> Result { + Uuid::parse_str(&raw) + .map(Self) + .map_err(|_| ConsultRequestError::InvalidSessionHandle(raw)) + } + + fn display(&self) -> String { + self.0.to_string() + } +} + #[derive(Debug, Error)] enum ConsultRequestError { #[error("prompt must not be empty")] @@ -200,6 +231,8 @@ enum ConsultRequestError { NotDirectory(String), #[error("max_turns must be greater than zero")] InvalidTurnLimit, + #[error("session_id must be a valid UUID, got `{0}`")] + InvalidSessionHandle(String), } #[derive(Debug, Error)] @@ -317,6 +350,9 @@ fn invoke_claude(request: &ConsultRequest) -> Result String { +fn concise_text(request: &ConsultRequest, response: &ConsultResponse) -> String { let mut status = vec![ "consult ok".to_owned(), + format!("session={}", request.session_mode()), format!("turns={}", response.num_turns), format!("duration={}", render_duration_ms(response.duration_ms)), ]; @@ -456,6 +497,9 @@ fn concise_text(response: &ConsultResponse) -> String { let mut lines = vec![status.join(" ")]; lines.push(format!("cwd: {}", response.cwd.display())); + if let Some(session_id) = request.requested_session_id() { + lines.push(format!("requested_session: {session_id}")); + } if let Some(session_id) = response.session_id.as_deref() { lines.push(format!("session: {session_id}")); } @@ -470,12 +514,19 @@ fn concise_text(response: &ConsultResponse) -> String { lines.join("\n") } -fn full_text(response: &ConsultResponse) -> String { +fn full_text(request: &ConsultRequest, response: &ConsultResponse) -> String { let mut lines = vec![ - format!("consult ok turns={}", response.num_turns), + format!( + "consult ok session={} turns={}", + request.session_mode(), + response.num_turns + ), format!("cwd: {}", response.cwd.display()), format!("duration: {}", render_duration_ms(response.duration_ms)), ]; + if let Some(session_id) = request.requested_session_id() { + lines.push(format!("requested_session: {session_id}")); + } if let Some(duration_api_ms) = response.duration_api_ms { lines.push(format!( "api_duration: {}", -- cgit v1.2.3