Skip to content

Commit 86afdea

Browse files
matt2ebaxenclaude
authored
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>
1 parent 97671f3 commit 86afdea

19 files changed

Lines changed: 287 additions & 411 deletions

File tree

crates/goose-sdk/src/custom_requests.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,15 @@ pub struct UpdateSessionProjectRequest {
222222
pub project_id: Option<String>,
223223
}
224224

225+
/// Rename a session.
226+
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
227+
#[request(method = "_goose/session/rename", response = EmptyResponse)]
228+
#[serde(rename_all = "camelCase")]
229+
pub struct RenameSessionRequest {
230+
pub session_id: String,
231+
pub title: String,
232+
}
233+
225234
/// Archive a session (soft delete).
226235
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
227236
#[request(method = "_goose/session/archive", response = EmptyResponse)]

crates/goose/acp-meta.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@
110110
"requestType": "UpdateSessionProjectRequest",
111111
"responseType": "EmptyResponse"
112112
},
113+
{
114+
"method": "_goose/session/rename",
115+
"requestType": "RenameSessionRequest",
116+
"responseType": "EmptyResponse"
117+
},
113118
{
114119
"method": "_goose/session/archive",
115120
"requestType": "ArchiveSessionRequest",

crates/goose/acp-schema.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,24 @@
743743
"x-side": "agent",
744744
"x-method": "_goose/session/update_project"
745745
},
746+
"RenameSessionRequest": {
747+
"type": "object",
748+
"properties": {
749+
"sessionId": {
750+
"type": "string"
751+
},
752+
"title": {
753+
"type": "string"
754+
}
755+
},
756+
"required": [
757+
"sessionId",
758+
"title"
759+
],
760+
"description": "Rename a session.",
761+
"x-side": "agent",
762+
"x-method": "_goose/session/rename"
763+
},
746764
"ArchiveSessionRequest": {
747765
"type": "object",
748766
"properties": {
@@ -1582,6 +1600,15 @@
15821600
"description": "Params for _goose/session/update_project",
15831601
"title": "UpdateSessionProjectRequest"
15841602
},
1603+
{
1604+
"allOf": [
1605+
{
1606+
"$ref": "#/$defs/RenameSessionRequest"
1607+
}
1608+
],
1609+
"description": "Params for _goose/session/rename",
1610+
"title": "RenameSessionRequest"
1611+
},
15851612
{
15861613
"allOf": [
15871614
{

crates/goose/src/acp/server.rs

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -171,20 +171,51 @@ fn sid_short(id: &str) -> String {
171171
}
172172

173173
fn thread_session_meta(
174-
message_count: i64,
175-
metadata: &crate::session::ThreadMetadata,
174+
thread: &crate::session::Thread,
176175
) -> serde_json::Map<String, serde_json::Value> {
177176
let mut meta = serde_json::Map::new();
178177
meta.insert(
179178
"messageCount".to_string(),
180-
serde_json::Value::Number(message_count.into()),
179+
serde_json::Value::Number(thread.message_count.into()),
181180
);
182-
if let Some(ref pid) = metadata.project_id {
181+
meta.insert(
182+
"createdAt".to_string(),
183+
serde_json::Value::String(thread.created_at.to_rfc3339()),
184+
);
185+
if let Some(ref archived_at) = thread.archived_at {
186+
meta.insert(
187+
"archivedAt".to_string(),
188+
serde_json::Value::String(archived_at.to_rfc3339()),
189+
);
190+
}
191+
meta.insert(
192+
"userSetName".to_string(),
193+
serde_json::Value::Bool(thread.user_set_name),
194+
);
195+
if let Some(ref pid) = thread.metadata.project_id {
183196
meta.insert(
184197
"projectId".to_string(),
185198
serde_json::Value::String(pid.clone()),
186199
);
187200
}
201+
if let Some(ref provider_id) = thread.metadata.provider_id {
202+
meta.insert(
203+
"providerId".to_string(),
204+
serde_json::Value::String(provider_id.clone()),
205+
);
206+
}
207+
if let Some(ref model_id) = thread.metadata.model_id {
208+
meta.insert(
209+
"modelId".to_string(),
210+
serde_json::Value::String(model_id.clone()),
211+
);
212+
}
213+
if let Some(ref persona_id) = thread.metadata.persona_id {
214+
meta.insert(
215+
"personaId".to_string(),
216+
serde_json::Value::String(persona_id.clone()),
217+
);
218+
}
188219
meta
189220
}
190221

@@ -1641,10 +1672,18 @@ impl GooseAcpAgent {
16411672
.and_then(|v| v.as_str())
16421673
.map(|s| s.to_string());
16431674

1675+
let persona_id = args
1676+
.meta
1677+
.as_ref()
1678+
.and_then(|m| m.get("personaId"))
1679+
.and_then(|v| v.as_str())
1680+
.map(|s| s.to_string());
1681+
16441682
// Create the Thread — this IS the ACP session from the client's perspective.
16451683
let thread_metadata = crate::session::ThreadMetadata {
16461684
provider_id: requested_provider.clone(),
16471685
project_id,
1686+
persona_id,
16481687
mode: Some(self.goose_mode.to_string()),
16491688
..Default::default()
16501689
};
@@ -2188,6 +2227,21 @@ impl GooseAcpAgent {
21882227
let t_start = std::time::Instant::now();
21892228
debug!(target: "perf", sid = %sid, "perf: prompt start");
21902229

2230+
// Update persona_id on the thread if the client sent one in _meta.
2231+
let prompt_persona_id = args
2232+
.meta
2233+
.as_ref()
2234+
.and_then(|m| m.get("personaId"))
2235+
.and_then(|v| v.as_str())
2236+
.map(|s| s.to_string());
2237+
if let Some(ref pid) = prompt_persona_id {
2238+
let pid = pid.clone();
2239+
self.update_thread_metadata(&thread_id, move |meta| {
2240+
meta.persona_id = Some(pid);
2241+
})
2242+
.await?;
2243+
}
2244+
21912245
let cancel_token = CancellationToken::new();
21922246
let internal_session_id = self.internal_session_id(&thread_id).await?;
21932247

@@ -2413,7 +2467,7 @@ impl GooseAcpAgent {
24132467
let t_step = std::time::Instant::now();
24142468
let model_id_owned = model_id.to_string();
24152469
self.update_thread_metadata(thread_id, move |meta| {
2416-
meta.model_name = Some(model_id_owned);
2470+
meta.model_id = Some(model_id_owned);
24172471
})
24182472
.await?;
24192473
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 {
26352689
let provider_name_owned = provider_name.to_string();
26362690
self.update_thread_metadata(thread_id, move |meta| {
26372691
meta.provider_id = Some(provider_name_owned);
2692+
meta.model_id = None;
26382693
})
26392694
.await?;
26402695
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 {
27012756
.as_deref()
27022757
.map(std::path::PathBuf::from)
27032758
.unwrap_or_default();
2704-
let meta = thread_session_meta(t.message_count, &t.metadata);
2759+
let meta = thread_session_meta(&t);
27052760
SessionInfo::new(SessionId::new(t.id), cwd)
27062761
.title(t.name)
27072762
.updated_at(t.updated_at.to_rfc3339())
@@ -2765,7 +2820,7 @@ impl GooseAcpAgent {
27652820
},
27662821
);
27672822

2768-
let meta = thread_session_meta(new_thread.message_count, &new_thread.metadata);
2823+
let meta = thread_session_meta(&new_thread);
27692824

27702825
let mut response = ForkSessionResponse::new(SessionId::new(new_thread_id))
27712826
.modes(mode_state)
@@ -3275,6 +3330,18 @@ impl GooseAcpAgent {
32753330
Ok(EmptyResponse {})
32763331
}
32773332

3333+
#[custom_method(RenameSessionRequest)]
3334+
async fn on_rename_session(
3335+
&self,
3336+
req: RenameSessionRequest,
3337+
) -> Result<EmptyResponse, sacp::Error> {
3338+
self.thread_manager
3339+
.update_thread(&req.session_id, Some(req.title), Some(true), None)
3340+
.await
3341+
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
3342+
Ok(EmptyResponse {})
3343+
}
3344+
32783345
#[custom_method(ArchiveSessionRequest)]
32793346
async fn on_archive_session(
32803347
&self,

crates/goose/src/session/thread_manager.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ pub struct ThreadMetadata {
3030
pub project_id: Option<String>,
3131
#[serde(default)]
3232
pub provider_id: Option<String>,
33-
#[serde(default)]
34-
pub model_name: Option<String>,
33+
#[serde(default, alias = "model_name")]
34+
pub model_id: Option<String>,
3535
#[serde(default)]
3636
pub mode: Option<String>,
3737
#[serde(flatten)]

crates/goose/tests/acp_common_tests/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,18 @@ pub async fn run_list_sessions<C: Connection>() {
5757
let mut response = conn.list_sessions().await.unwrap();
5858
for s in &mut response.sessions {
5959
s.updated_at = None;
60+
// createdAt is a dynamic timestamp — verify it exists then remove for comparison.
61+
if let Some(ref mut meta) = s.meta {
62+
assert!(meta.get("createdAt").and_then(|v| v.as_str()).is_some());
63+
meta.remove("createdAt");
64+
}
6065
}
6166
let mut expected_meta = serde_json::Map::new();
6267
expected_meta.insert(
6368
"messageCount".to_string(),
6469
serde_json::Value::Number(2.into()),
6570
);
71+
expected_meta.insert("userSetName".to_string(), serde_json::Value::Bool(false));
6672
assert_eq!(
6773
response,
6874
ListSessionsResponse::new(vec![SessionInfo::new(

ui/goose2/src/app/AppShell.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
276276
perfLog(
277277
`[perf:newtab] createNewTab start (project=${project?.id ?? "none"})`,
278278
);
279-
const agentId = agentStore.activeAgentId ?? undefined;
280279
const providerId =
281280
project?.preferredProvider ?? agentStore.selectedProvider ?? "goose";
282281
const sessionModelPreference =
@@ -312,7 +311,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
312311
const session = await sessionStore.createSession({
313312
title,
314313
projectId: project?.id,
315-
agentId,
316314
providerId: sessionModelPreference.providerId,
317315
workingDir,
318316
modelId: sessionModelPreference.modelId,
@@ -327,7 +325,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
327325
return session;
328326
},
329327
[
330-
agentStore.activeAgentId,
331328
agentStore.selectedProvider,
332329
chatStore,
333330
providerInventoryEntries,

0 commit comments

Comments
 (0)