feat: migrate session metadata storage from frontend overlay to backend (#8769)
Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -222,6 +222,15 @@ pub struct UpdateSessionProjectRequest {
|
||||
pub project_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Rename a session.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/session/rename", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RenameSessionRequest {
|
||||
pub session_id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// Archive a session (soft delete).
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/session/archive", response = EmptyResponse)]
|
||||
|
||||
@@ -110,6 +110,11 @@
|
||||
"requestType": "UpdateSessionProjectRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/session/rename",
|
||||
"requestType": "RenameSessionRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/session/archive",
|
||||
"requestType": "ArchiveSessionRequest",
|
||||
|
||||
@@ -743,6 +743,24 @@
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/session/update_project"
|
||||
},
|
||||
"RenameSessionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessionId",
|
||||
"title"
|
||||
],
|
||||
"description": "Rename a session.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/session/rename"
|
||||
},
|
||||
"ArchiveSessionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1582,6 +1600,15 @@
|
||||
"description": "Params for _goose/session/update_project",
|
||||
"title": "UpdateSessionProjectRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RenameSessionRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/session/rename",
|
||||
"title": "RenameSessionRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -171,20 +171,51 @@ fn sid_short(id: &str) -> String {
|
||||
}
|
||||
|
||||
fn thread_session_meta(
|
||||
message_count: i64,
|
||||
metadata: &crate::session::ThreadMetadata,
|
||||
thread: &crate::session::Thread,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut meta = serde_json::Map::new();
|
||||
meta.insert(
|
||||
"messageCount".to_string(),
|
||||
serde_json::Value::Number(message_count.into()),
|
||||
serde_json::Value::Number(thread.message_count.into()),
|
||||
);
|
||||
if let Some(ref pid) = metadata.project_id {
|
||||
meta.insert(
|
||||
"createdAt".to_string(),
|
||||
serde_json::Value::String(thread.created_at.to_rfc3339()),
|
||||
);
|
||||
if let Some(ref archived_at) = thread.archived_at {
|
||||
meta.insert(
|
||||
"archivedAt".to_string(),
|
||||
serde_json::Value::String(archived_at.to_rfc3339()),
|
||||
);
|
||||
}
|
||||
meta.insert(
|
||||
"userSetName".to_string(),
|
||||
serde_json::Value::Bool(thread.user_set_name),
|
||||
);
|
||||
if let Some(ref pid) = thread.metadata.project_id {
|
||||
meta.insert(
|
||||
"projectId".to_string(),
|
||||
serde_json::Value::String(pid.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(ref provider_id) = thread.metadata.provider_id {
|
||||
meta.insert(
|
||||
"providerId".to_string(),
|
||||
serde_json::Value::String(provider_id.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(ref model_id) = thread.metadata.model_id {
|
||||
meta.insert(
|
||||
"modelId".to_string(),
|
||||
serde_json::Value::String(model_id.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(ref persona_id) = thread.metadata.persona_id {
|
||||
meta.insert(
|
||||
"personaId".to_string(),
|
||||
serde_json::Value::String(persona_id.clone()),
|
||||
);
|
||||
}
|
||||
meta
|
||||
}
|
||||
|
||||
@@ -1641,10 +1672,18 @@ impl GooseAcpAgent {
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let persona_id = args
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("personaId"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Create the Thread — this IS the ACP session from the client's perspective.
|
||||
let thread_metadata = crate::session::ThreadMetadata {
|
||||
provider_id: requested_provider.clone(),
|
||||
project_id,
|
||||
persona_id,
|
||||
mode: Some(self.goose_mode.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -2188,6 +2227,21 @@ impl GooseAcpAgent {
|
||||
let t_start = std::time::Instant::now();
|
||||
debug!(target: "perf", sid = %sid, "perf: prompt start");
|
||||
|
||||
// Update persona_id on the thread if the client sent one in _meta.
|
||||
let prompt_persona_id = args
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("personaId"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
if let Some(ref pid) = prompt_persona_id {
|
||||
let pid = pid.clone();
|
||||
self.update_thread_metadata(&thread_id, move |meta| {
|
||||
meta.persona_id = Some(pid);
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
let cancel_token = CancellationToken::new();
|
||||
let internal_session_id = self.internal_session_id(&thread_id).await?;
|
||||
|
||||
@@ -2413,7 +2467,7 @@ impl GooseAcpAgent {
|
||||
let t_step = std::time::Instant::now();
|
||||
let model_id_owned = model_id.to_string();
|
||||
self.update_thread_metadata(thread_id, move |meta| {
|
||||
meta.model_name = Some(model_id_owned);
|
||||
meta.model_id = Some(model_id_owned);
|
||||
})
|
||||
.await?;
|
||||
debug!(target: "perf", sid = %sid, ms = t_step.elapsed().as_millis() as u64, "perf: set_model update_thread_metadata");
|
||||
@@ -2635,6 +2689,7 @@ impl GooseAcpAgent {
|
||||
let provider_name_owned = provider_name.to_string();
|
||||
self.update_thread_metadata(thread_id, move |meta| {
|
||||
meta.provider_id = Some(provider_name_owned);
|
||||
meta.model_id = None;
|
||||
})
|
||||
.await?;
|
||||
debug!(target: "perf", sid = %sid, ms = t_step.elapsed().as_millis() as u64, "perf: update_provider update_thread_metadata");
|
||||
@@ -2701,7 +2756,7 @@ impl GooseAcpAgent {
|
||||
.as_deref()
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_default();
|
||||
let meta = thread_session_meta(t.message_count, &t.metadata);
|
||||
let meta = thread_session_meta(&t);
|
||||
SessionInfo::new(SessionId::new(t.id), cwd)
|
||||
.title(t.name)
|
||||
.updated_at(t.updated_at.to_rfc3339())
|
||||
@@ -2765,7 +2820,7 @@ impl GooseAcpAgent {
|
||||
},
|
||||
);
|
||||
|
||||
let meta = thread_session_meta(new_thread.message_count, &new_thread.metadata);
|
||||
let meta = thread_session_meta(&new_thread);
|
||||
|
||||
let mut response = ForkSessionResponse::new(SessionId::new(new_thread_id))
|
||||
.modes(mode_state)
|
||||
@@ -3275,6 +3330,18 @@ impl GooseAcpAgent {
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method(RenameSessionRequest)]
|
||||
async fn on_rename_session(
|
||||
&self,
|
||||
req: RenameSessionRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.thread_manager
|
||||
.update_thread(&req.session_id, Some(req.title), Some(true), None)
|
||||
.await
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method(ArchiveSessionRequest)]
|
||||
async fn on_archive_session(
|
||||
&self,
|
||||
|
||||
@@ -30,8 +30,8 @@ pub struct ThreadMetadata {
|
||||
pub project_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub provider_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_name: Option<String>,
|
||||
#[serde(default, alias = "model_name")]
|
||||
pub model_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub mode: Option<String>,
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -57,12 +57,18 @@ pub async fn run_list_sessions<C: Connection>() {
|
||||
let mut response = conn.list_sessions().await.unwrap();
|
||||
for s in &mut response.sessions {
|
||||
s.updated_at = None;
|
||||
// createdAt is a dynamic timestamp — verify it exists then remove for comparison.
|
||||
if let Some(ref mut meta) = s.meta {
|
||||
assert!(meta.get("createdAt").and_then(|v| v.as_str()).is_some());
|
||||
meta.remove("createdAt");
|
||||
}
|
||||
}
|
||||
let mut expected_meta = serde_json::Map::new();
|
||||
expected_meta.insert(
|
||||
"messageCount".to_string(),
|
||||
serde_json::Value::Number(2.into()),
|
||||
);
|
||||
expected_meta.insert("userSetName".to_string(), serde_json::Value::Bool(false));
|
||||
assert_eq!(
|
||||
response,
|
||||
ListSessionsResponse::new(vec![SessionInfo::new(
|
||||
|
||||
Reference in New Issue
Block a user