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:
Matt Toohey
2026-04-24 15:28:48 +12:00
committed by GitHub
parent 97671f30b0
commit 86afdeab6a
19 changed files with 287 additions and 411 deletions
+9
View File
@@ -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)]
+5
View File
@@ -110,6 +110,11 @@
"requestType": "UpdateSessionProjectRequest",
"responseType": "EmptyResponse"
},
{
"method": "_goose/session/rename",
"requestType": "RenameSessionRequest",
"responseType": "EmptyResponse"
},
{
"method": "_goose/session/archive",
"requestType": "ArchiveSessionRequest",
+27
View File
@@ -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": [
{
+74 -7
View File
@@ -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,
+2 -2
View File
@@ -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(