swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/phone-opus/src
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-23 15:38:00 -0400
committermain <main@swarm.moe>2026-03-23 15:38:00 -0400
commitff76c72f3c78694eebe4824318e85c4751343cf4 (patch)
treecfb49d55874a9f90b11fed9756f00e1fc233faeb /crates/phone-opus/src
parent36f3be895696e674fced66ef2b6d285149ee5562 (diff)
downloadphone_opus-ff76c72f3c78694eebe4824318e85c4751343cf4.zip
Support resuming Claude consult sessions
Diffstat (limited to 'crates/phone-opus/src')
-rw-r--r--crates/phone-opus/src/mcp/catalog.rs6
-rw-r--r--crates/phone-opus/src/mcp/service.rs61
2 files changed, 61 insertions, 6 deletions
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<String>,
max_turns: Option<u64>,
+ session_id: Option<String>,
}
#[derive(Debug, Clone)]
@@ -107,6 +109,7 @@ struct ConsultRequest {
prompt: PromptText,
cwd: WorkingDirectory,
max_turns: Option<TurnLimit>,
+ session: Option<SessionHandle>,
}
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<String> {
+ 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<Self, ConsultRequestError> {
+ 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<ConsultResponse, ConsultInv
.arg(CLAUDE_TOOLSET)
.arg("--permission-mode")
.arg("dontAsk");
+ if let Some(session) = request.session.as_ref() {
+ let _ = command.arg("--resume").arg(session.display());
+ }
if let Some(max_turns) = request.max_turns {
let _ = command.arg("--max-turns").arg(max_turns.get().to_string());
}
@@ -401,6 +437,8 @@ fn consult_output(
let concise = json!({
"response": response.result,
"cwd": response.cwd.display(),
+ "session_mode": request.session_mode(),
+ "requested_session_id": request.requested_session_id(),
"model": response.model_name(),
"duration_ms": response.duration_ms,
"num_turns": response.num_turns,
@@ -414,6 +452,8 @@ fn consult_output(
"cwd": response.cwd.display(),
"prompt": request.prompt.as_str(),
"max_turns": request.max_turns.map(TurnLimit::get),
+ "session_mode": request.session_mode(),
+ "requested_session_id": request.requested_session_id(),
"duration_ms": response.duration_ms,
"duration_api_ms": response.duration_api_ms,
"num_turns": response.num_turns,
@@ -429,8 +469,8 @@ fn consult_output(
fallback_detailed_tool_output(
&concise,
&full,
- concise_text(response),
- Some(full_text(response)),
+ concise_text(request, response),
+ Some(full_text(request, response)),
SurfaceKind::Read,
generation,
FaultStage::Worker,
@@ -438,9 +478,10 @@ fn consult_output(
)
}
-fn concise_text(response: &ConsultResponse) -> 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: {}",