Skip to content

Session manager#4648

Merged
DOsinga merged 32 commits intomainfrom
session-manager
Sep 26, 2025
Merged

Session manager#4648
DOsinga merged 32 commits intomainfrom
session-manager

Conversation

@DOsinga
Copy link
Copy Markdown
Collaborator

@DOsinga DOsinga commented Sep 15, 2025

Use SQLite for Session manager

Don't implement your own database

@jamadeo jamadeo requested a review from Copilot September 22, 2025 13:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request implements a comprehensive migration from a file-based session storage system to an SQLite database for session management. The primary purpose is to replace the custom JSONL file-based session storage with a more robust, standardized database approach.

Key changes:

  • Replace custom JSONL session storage with SQLite database using SessionManager
  • Migrate all session-related APIs to use the new database schema
  • Remove deprecated file-based session functions and replace with database operations

Reviewed Changes

Copilot reviewed 40 out of 42 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
ui/desktop/src/sessions.ts Removes custom session management functions (updateSessionMetadata, deleteSession)
ui/desktop/src/goosed.ts Adds error logging and removes server status check for external backend
ui/desktop/src/components/sessions/SessionsInsights.tsx Updates to use new API session insights instead of custom fetch
ui/desktop/src/components/sessions/SessionListView.tsx Migrates to use API functions for session operations
ui/desktop/src/api/types.gen.ts Adds new API types for session insights and metadata operations
ui/desktop/src/api/sdk.gen.ts Adds generated API functions for session management
crates/goose/src/session/session_manager.rs New SQLite-based session manager implementation
crates/goose/src/session/legacy.rs Legacy session file reader for migration purposes
crates/goose/src/agents/agent.rs Updates agent code to use SessionManager instead of file operations
crates/goose-server/src/routes/session.rs Updates server routes to use SessionManager
crates/goose-cli/src/session/mod.rs Updates CLI session handling to use SessionManager

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

let storage = Arc::new(SessionStorage::new().await?);
match SESSION_STORAGE.set(storage.clone()) {
Ok(_) => Ok(storage),
Err(_) => Ok(SESSION_STORAGE.get().unwrap().clone()),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the LLMs said:

async fn instance() -> Result<Arc> {
let storage = Arc::new(SessionStorage::new().await?);
match SESSION_STORAGE.set(storage) {
Ok() => Ok(SESSION_STORAGE.get().unwrap().clone()),
Err(
) => Ok(SESSION_STORAGE.get().unwrap().clone()),
}
}

should do the trick, since failure means we already have the thing

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's true for a regular OnceCell, but this is a tokio::OnceCell, which can fail to set if it's being concurrently set

If the OnceCell is empty, but some other task is currently trying to set the value, this call will fail with SetError::InitializingError.

{
error!("Failed to persist compacted messages: {}", e);
}
tracing::info!("History replaced, compacting happened in reply");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not in scope here, but have we considered how compaction should work with session history? seems like we should store the both the old and compacted state of things

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea is that we keep all messages around but annotate them with visibility. Hmm, is this already in /cc @katzdave

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we have message metadata tagging messages appropriately as agent vs user visible.

I could see benefits of even more metadata tags like 'summary' being relevant.

I'm pushing this domain actively both in the UI for making the post/pre-summary messages more clear, and I have some other ideas around improving the summaries with with keeping around more user messages + messages at the end of tool call chains.


session = SessionManager::get_session(&session.id, false)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do something other than throw away the error here? maybe at minimum a log

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why particularly here? this should never fail I think - we're just refreshing the session after updating it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we leave it for another error handling PR. I just think returning 500s without error messages isn't great because there's always an extra step when debugging

.0
.iter()
.zip(&other.0)
.all(|(a, b)| a.role == b.role && a.created == b.created && a.content == b.content)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we not defer to equivalence of the Messages ?

@DOsinga DOsinga requested a review from Copilot September 23, 2025 21:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 41 out of 43 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

crates/goose/src/agents/agent.rs:1

  • This TODO indicates that database updates are missing when handling interruptions. This could lead to session state inconsistencies and should be addressed.
use std::collections::HashMap;

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

match SessionManager::create_session(
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
"Web session".to_string(),
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest something like

let session = SessionManager::create_session(..).await.map_err(..)?;
Redirect::permanent(..)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, also shouldn't be a permanent redirect I think

)
.await
.unwrap();
Some(session.id)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function should probably not panic or exit(1) but that's too much yak shaving

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the cli though so it is not the end of the world (well, for the cli it is) - but I agree since who knows who might thing it is a good idea to call this builder. pre-existing ...

.await
.map(|session| session.conversation.unwrap_or_default())
.unwrap_or_else(|e| {
eprintln!("Warning: Failed to load message history: {}", e);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this isn't new but seems weird that this would just warn and continue

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so now you do want to panic? :)

let storage = Arc::new(SessionStorage::new().await?);
match SESSION_STORAGE.set(storage.clone()) {
Ok(_) => Ok(storage),
Err(_) => Ok(SESSION_STORAGE.get().unwrap().clone()),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's true for a regular OnceCell, but this is a tokio::OnceCell, which can fail to set if it's being concurrently set

If the OnceCell is empty, but some other task is currently trying to set the value, this call will fail with SetError::InitializingError.

let current_version = self.get_schema_version().await?;

if current_version < CURRENT_SCHEMA_VERSION {
println!(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we configure the logging utility so all output from these modules goes somewhere like:

/Users/alexhancock/.local/state/goose/logs/session-db

alongside the others?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced the println!

println!("Creating new session database...");
let storage = Self::create(&db_path).await?;

if let Err(e) = storage.import_legacy(&session_dir).await {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to test this with a few real session dirs from the wild to ensure no issues before launch

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imported 8000 from my folder...

.await?
.unwrap_or(0);

let session_id = format!("{}_{}", today, max_idx + 1);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever use the fact that the ids have these dates in them? If not, could just be an autoincrementing int in sqlite.

let modified_time = file_metadata.modified().unwrap_or(SystemTime::now());
let created_time = file_metadata
.created()
.unwrap_or_else(|_| parse_session_timestamp(session_name).unwrap_or(modified_time));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were the old session names human readable title strings? or is this a different field from the metadata and called session_name in this code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah, we just called it that. they were date strings. description is the human readable version

s.id === sessionId
? { ...s, metadata: { ...s.metadata, description: newDescription } }
: s
s.id === sessionId ? { ...s, metadata: { ...s, description: newDescription } } : s
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need all the props at both root and metadata of this object? it seems like how we've flattened out the data model a lot this object could become flattened as well

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops. there is no more metadata, this is just a remnant that will set this useless property that we then don't display until we get confirmation from the server. nice catch!

@alexhancock alexhancock self-requested a review September 26, 2025 15:25
Copy link
Copy Markdown
Collaborator

@alexhancock alexhancock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some conflicts but once resolved 🚢

@DOsinga
Copy link
Copy Markdown
Collaborator Author

DOsinga commented Sep 26, 2025

Looks like some conflicts but once resolved 🚢

yeah, with this type of thing, I'd rather let the conflicts vester than keep updating them

@DOsinga DOsinga merged commit dc29288 into main Sep 26, 2025
10 checks passed
@DOsinga DOsinga deleted the session-manager branch September 26, 2025 18:56
HikaruEgashira pushed a commit to HikaruEgashira/goose that referenced this pull request Oct 3, 2025
Co-authored-by: Douwe Osinga <[email protected]>
Signed-off-by: HikaruEgashira <[email protected]>
Comment on lines +131 to +138
let messages = if let Some(session_id) = &session_id {
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
SessionManager::get_session(session_id, true)
.await
.map(|session| session.conversation.unwrap_or_default())
.unwrap()
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, @DOsinga! Since this got merged, I started seeing this error:

thread 'main' panicked at crates/goose-cli/src/session/mod.rs:136:26:
called `Result::unwrap()` on an `Err` value: Session not found
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if a good patch, but this fixed the issue for me locally: #5055

r0x0d added a commit to r0x0d/goose that referenced this pull request Oct 7, 2025
This bug got introduced in when merging  dc29288 (block#4648).  The problem was that the code was trying to load a session id from the database that does not exist.
r0x0d added a commit to r0x0d/goose that referenced this pull request Oct 7, 2025
This bug got introduced in when merging  dc29288 (block#4648).  The problem was that the code was trying to load a session id from the database that does not exist.

Signed-off-by: Rodolfo Olivieri <[email protected]>
r0x0d added a commit to r0x0d/goose that referenced this pull request Oct 14, 2025
This bug got introduced in when merging  dc29288 (block#4648).  The problem was that the code was trying to load a session id from the database that does not exist.

Signed-off-by: Rodolfo Olivieri <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants