diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index bc9468a759c1..46ac740acb8f 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -116,7 +116,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} artifacts: | goose-*.tar.bz2 - goose-*.zip + goose*.zip *.deb *.rpm download_cli.sh diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 4f09015d1585..58e2d31aaea6 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -118,7 +118,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} artifacts: | goose-*.tar.bz2 - goose-*.zip + goose*.zip *.deb *.rpm download_cli.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8de131bc519..2cd9e2635199 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,7 +105,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} artifacts: | goose-*.tar.bz2 - goose-*.zip + goose*.zip *.deb *.rpm download_cli.sh @@ -122,7 +122,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} artifacts: | goose-*.tar.bz2 - goose-*.zip + goose*.zip *.deb *.rpm download_cli.sh diff --git a/Cargo.lock b/Cargo.lock index 697d048898b8..201d39bdbaaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.9.0" +version = "1.10.2" dependencies = [ "ahash", "anyhow", @@ -2657,7 +2657,7 @@ dependencies = [ [[package]] name = "goose-bench" -version = "1.9.0" +version = "1.10.2" dependencies = [ "anyhow", "async-trait", @@ -2680,7 +2680,7 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.9.0" +version = "1.10.2" dependencies = [ "agent-client-protocol", "anstream", @@ -2732,7 +2732,7 @@ dependencies = [ [[package]] name = "goose-mcp" -version = "1.9.0" +version = "1.10.2" dependencies = [ "anyhow", "async-trait", @@ -2798,7 +2798,7 @@ dependencies = [ [[package]] name = "goose-server" -version = "1.9.0" +version = "1.10.2" dependencies = [ "anyhow", "async-trait", @@ -2836,7 +2836,7 @@ dependencies = [ [[package]] name = "goose-test" -version = "1.9.0" +version = "1.10.2" dependencies = [ "clap", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c84d942ae194..d724b1b994ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.9.0" +version = "1.10.2" authors = ["Block "] license = "Apache-2.0" repository = "https://github.com/block/goose" diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index 283c52f434f1..9aa6958407a4 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -354,6 +354,7 @@ impl SessionStorage { .filename(db_path) .create_if_missing(create_if_missing) .busy_timeout(std::time::Duration::from_secs(5)) + .shared_cache(true) .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); sqlx::SqlitePool::connect_with(options).await.map_err(|e| { @@ -1116,4 +1117,171 @@ mod tests { assert_eq!(conversation.messages()[0].role, Role::User); assert_eq!(conversation.messages()[1].role, Role::Assistant); } + + /// Test for WAL mode race condition matching build_session() pattern + /// + /// This test closely simulates the actual build_session() flow: + /// 1. Determine if we need to create a new session (session_id is None) + /// 2. Call create_session() to create it + /// 3. Get the returned session_id + /// 4. Immediately call get_session() with that id (like CliSession::new does) + /// + /// This matches the code in builder.rs:260-280 and mod.rs:138-149 + #[tokio::test] + async fn test_wal_race_condition_create_then_get() { + use tokio::sync::Barrier; + use std::time::Duration; + + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test_wal_race.db"); + let storage = Arc::new(SessionStorage::create(&db_path).await.unwrap()); + + const NUM_TASKS: usize = 100; + let barrier = Arc::new(Barrier::new(NUM_TASKS)); + let mut handles = vec![]; + + for i in 0..NUM_TASKS { + let storage = Arc::clone(&storage); + let barrier = Arc::clone(&barrier); + + let handle = tokio::spawn(async move { + // Wait for all tasks to be ready + barrier.wait().await; + + // Simulate build_session() logic: + // Step 1: session_id is None, so we need to create a new session + let session_id: Option = None; + + // Step 2: Create session (like builder.rs:270-275) + let session_id = if session_id.is_none() { + let session = storage + .create_session( + PathBuf::from(format!("/tmp/test_{}", i)), + format!("Race test session {}", i) + ) + .await + .expect("Failed to create session"); + Some(session.id) + } else { + session_id + }; + + // Step 3: Now simulate CliSession::new() which immediately reads the session + // (like mod.rs:138-149) + let session_id = session_id.unwrap(); + + // This is the critical read that happens in CliSession::new + // It tries to load the conversation from the just-created session + let fetched = storage + .get_session(&session_id, true) // include_messages=true like real code + .await; + + match fetched { + Ok(fetched_session) => { + assert_eq!(fetched_session.id, session_id, + "Session ID mismatch for session {}", i); + println!("✅ share SUCCESS: Session {} found immediately after creation", session_id); + Ok(session_id) + } + Err(e) => { + // This is the race condition we're testing for + eprintln!("⚠️ share RACE DETECTED: Session {} not found immediately after creation: {}", + session_id, e); + Err(format!("Session {} not found: {}", session_id, e)) + } + } + }); + + handles.push(handle); + } + + // Collect results + let mut errors = vec![]; + for handle in handles { + match handle.await.unwrap() { + Ok(_) => {}, + Err(e) => errors.push(e), + } + } + + // Give WAL time to checkpoint + tokio::time::sleep(Duration::from_millis(100)).await; + + // Report any race conditions detected + if !errors.is_empty() { + panic!( + "WAL race condition detected in {} out of {} tasks:\n{}", + errors.len(), + NUM_TASKS, + errors.join("\n") + ); + } + } + + /// Test the exact pattern used in CliSession::new with block_in_place + /// + /// This test simulates the blocking pattern used in the actual code to see + /// if it exacerbates the WAL race condition. + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn test_wal_race_with_blocking_pattern() { + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test_blocking_race.db"); + let storage = Arc::new(SessionStorage::create(&db_path).await.unwrap()); + + const NUM_ITERATIONS: usize = 50; + let mut handles = vec![]; + + for i in 0..NUM_ITERATIONS { + let storage = Arc::clone(&storage); + + let handle = tokio::spawn(async move { + // Create a session + let description = format!("Blocking test {}", i); + let created = storage + .create_session(PathBuf::from(format!("/tmp/test_{}", i)), description) + .await + .unwrap(); + + // Simulate CliSession::new's blocking pattern + let session_id = created.id.clone(); + let fetched = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + storage.get_session(&session_id, false).await + }) + }); + + match fetched { + Ok(_) => { + println!("✅ SUCCESS (blocking): Session {} found immediately after creation", session_id); + Ok(created.id) + } + Err(e) => { + eprintln!("⚠️ RACE DETECTED with block_in_place: Session {} not found: {}", + session_id, e); + Err(format!("Session {} not found with blocking: {}", session_id, e)) + } + } + }); + + handles.push(handle); + } + + // Collect results + let mut errors = vec![]; + for handle in handles { + match handle.await.unwrap() { + Ok(_) => {}, + Err(e) => errors.push(e), + } + } + + if !errors.is_empty() { + panic!( + "WAL race condition detected with blocking pattern in {} out of {} iterations:\n{}", + errors.len(), + NUM_ITERATIONS, + errors.join("\n") + ); + } + } } diff --git a/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper b/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper index 158e12c08372..7972a960660a 100644 --- a/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper +++ b/crates/goose/tests/mcp_replays/cargorun--quiet-pgoose-server--bingoosed--mcpdeveloper @@ -1,17 +1,17 @@ -STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"1.9.0"}}} +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"1.10.0"}}} STDERR: 2025-09-27T04:13:30.409389Z  INFO goose_mcp::mcp_server_runner: Starting MCP server STDERR: at crates/goose-mcp/src/mcp_server_runner.rs:18 STDERR: STDERR: 2025-09-27T04:13:30.412663Z  INFO goose_mcp::developer::analyze::cache: Initializing analysis cache with size 100 STDERR: at crates/goose-mcp/src/developer/analyze/cache.rs:25 STDERR: -STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"prompts":{},"tools":{}},"serverInfo":{"name":"goose-developer","version":"1.9.0"},"instructions":" The developer extension gives you the capabilities to edit code files and run shell commands,\n and can be used to solve a wide range of problems.\n\nYou can use the shell tool to run any command that would work on the relevant operating system.\nUse the shell tool as needed to locate files or interact with the project.\n\nLeverage `analyze` through `return_last_only=true` subagents for deep codebase understanding with lean context\n- delegate analysis, retain summaries\n\nYour windows/screen tools can be used for visual debugging. You should not use these tools unless\nprompted to, but you can mention they are available if they are relevant.\n\nAlways prefer ripgrep (rg -C 3) to grep.\n\noperating system: macos\ncurrent directory: /Users/angiej/workspace/goose/crates/goose\n\n \n\nAdditional Text Editor Tool Instructions:\n\nPerform text editing operations on files.\n\nThe `command` parameter specifies the operation to perform. Allowed options are:\n- `view`: View the content of a file.\n- `write`: Create or overwrite a file with the given content\n- `str_replace`: Replace text in one or more files.\n- `insert`: Insert text at a specific line location in the file.\n- `undo_edit`: Undo the last edit made to a file.\n\nTo use the write command, you must specify `file_text` which will become the new content of the file. Be careful with\nexisting files! This is a full overwrite, so you must include everything - not just sections you are modifying.\n\nTo use the str_replace command to edit multiple files, use the `diff` parameter with a unified diff.\nTo use the str_replace command to edit one file, you must specify both `old_str` and `new_str` - the `old_str` needs to exactly match one\nunique section of the original file, including any whitespace. Make sure to include enough context that the match is not\nambiguous. The entire original string will be replaced with `new_str`\n\nWhen possible, batch file edits together by using a multi-file unified `diff` within a single str_replace tool call.\n\nTo use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning, -1 for end)\nand `new_str` (the text to insert).\n\n\n\nAdditional Shell Tool Instructions:\nExecute a command in the shell.\n\nThis will return the output and error concatenated into a single string, as\nyou would see from running on the command line. There will also be an indication\nof if the command succeeded or failed.\n\nAvoid commands that produce a large amount of output, and consider piping those outputs to files.\n\n**Important**: Each shell command runs in its own process. Things like directory changes or\nsourcing files do not persist between tool calls. So you may need to repeat them each time by\nstringing together commands.\nIf you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that\nthis tool does not run indefinitely.\n\n**Important**: Use ripgrep - `rg` - exclusively when you need to locate a file or a code reference,\nother solutions may produce too large output because of hidden files! For example *do not* use `find` or `ls -r`\n - List files by name: `rg --files | rg `\n - List files that contain a regex: `rg '' -l`\n\n - Multiple commands: Use && to chain commands, avoid newlines\n - Example: `cd example && ls` or `source env/bin/activate && pip install numpy`\n\n\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\nCloned Goose repo: /Users/angiej/workspace/goose\nMCP means Model Context Protocol. Docs: https://modelcontextprotocol.io/introduction\nUse GitHub CLI for GitHub-related tasks.\nWhen prompted for date-related information, do not rely on your internal knowledge for the current date. Instead, use the `date` terminal command to get the actual date and time.\nNEVER run blocking server commands (node server.js, npm start, etc.) - provide commands for user to run separately\n\n### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n# AGENTS Instructions\n\ngoose is an AI agent framework in Rust with CLI and Electron desktop interfaces.\n\n## Setup\n```bash\nsource bin/activate-hermit\ncargo build\n```\n\n## Commands\n\n### Build\n```bash\ncargo build # debug\ncargo build --release # release \njust release-binary # release + openapi\n```\n\n### Test\n```bash\ncargo test # all tests\ncargo test -p goose # specific crate\ncargo test --package goose --test mcp_integration_test\njust record-mcp-tests # record MCP\n```\n\n### Lint/Format\n```bash\ncargo fmt\n./scripts/clippy-lint.sh\ncargo clippy --fix\n```\n\n### UI\n```bash\njust generate-openapi # after server changes\njust run-ui # start desktop\ncd ui/desktop && npm test # test UI\n```\n\n## Structure\n```\ncrates/\n├── goose # core logic\n├── goose-bench # benchmarking\n├── goose-cli # CLI entry\n├── goose-server # backend (binary: goosed)\n├── goose-mcp # MCP extensions\n├── goose-test # test utilities\n├── mcp-client # MCP client\n├── mcp-core # MCP shared\n└── mcp-server # MCP server\n\ntemporal-service/ # Go scheduler\nui/desktop/ # Electron app\n```\n\n## Development Loop\n```bash\n# 1. source bin/activate-hermit\n# 2. Make changes\n# 3. cargo fmt\n# 4. cargo build\n# 5. cargo test -p \n# 6. ./scripts/clippy-lint.sh\n# 7. [if server] just generate-openapi\n```\n\n## Rules\n\nTest: Prefer tests/ folder, e.g. crates/goose/tests/\nError: Use anyhow::Result\nProvider: Implement Provider trait see providers/base.rs\nMCP: Extensions in crates/goose-mcp/\nServer: Changes need just generate-openapi\n\n## Never\n\nNever: Edit ui/desktop/openapi.json manually\nNever: Edit Cargo.toml use cargo add\nNever: Skip cargo fmt\nNever: Merge without ./scripts/clippy-lint.sh\n\n## Entry Points\n- CLI: crates/goose-cli/src/main.rs\n- Server: crates/goose-server/src/main.rs\n- UI: ui/desktop/src/main.ts\n- Agent: crates/goose/src/agents/agent.rs\n\nThis is a rust project with crates in the crates dir:\ngoose: the main code for goose, contains all the core logic\ngoose-bench: bench marking\ngoose-cli: the command line interface, use goose crate\ngoose-mcp: the mcp servers that ship with goose. the developer sub system is of special interest\ngoose-server: the server that suports the desktop (electron) app. also known as goosed\n\n\nui/desktop has an electron app in typescript. \n\nnon trivial features should be implemented in the goose crate and then be called from the goose-cli crate for the cli. for the desktop, you want to add routes to \ngoose-server/src/routes. you can then run `just generate-openapi` to generate the openapi spec which will modify the ui/desktop/src/api files. once you have\nthat you can call the functionality from the server from the typescript.\n\ntips: \n- can look at unstaged changes for what is being worked on if starting\n- always check rust compiles, cargo fmt etc and `./scripts/clippy-lint.sh` (as well as run tests in files you are working on)\n- in ui/desktop, look at how you can run lint checks and if other tests can run\n"}} +STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"prompts":{},"tools":{}},"serverInfo":{"name":"goose-developer","version":"1.10.0"},"instructions":" The developer extension gives you the capabilities to edit code files and run shell commands,\n and can be used to solve a wide range of problems.\n\nYou can use the shell tool to run any command that would work on the relevant operating system.\nUse the shell tool as needed to locate files or interact with the project.\n\nLeverage `analyze` through `return_last_only=true` subagents for deep codebase understanding with lean context\n- delegate analysis, retain summaries\n\nYour windows/screen tools can be used for visual debugging. You should not use these tools unless\nprompted to, but you can mention they are available if they are relevant.\n\nAlways prefer ripgrep (rg -C 3) to grep.\n\noperating system: macos\ncurrent directory: /Users/angiej/workspace/goose/crates/goose\n\n \n\nAdditional Text Editor Tool Instructions:\n\nPerform text editing operations on files.\n\nThe `command` parameter specifies the operation to perform. Allowed options are:\n- `view`: View the content of a file.\n- `write`: Create or overwrite a file with the given content\n- `str_replace`: Replace text in one or more files.\n- `insert`: Insert text at a specific line location in the file.\n- `undo_edit`: Undo the last edit made to a file.\n\nTo use the write command, you must specify `file_text` which will become the new content of the file. Be careful with\nexisting files! This is a full overwrite, so you must include everything - not just sections you are modifying.\n\nTo use the str_replace command to edit multiple files, use the `diff` parameter with a unified diff.\nTo use the str_replace command to edit one file, you must specify both `old_str` and `new_str` - the `old_str` needs to exactly match one\nunique section of the original file, including any whitespace. Make sure to include enough context that the match is not\nambiguous. The entire original string will be replaced with `new_str`\n\nWhen possible, batch file edits together by using a multi-file unified `diff` within a single str_replace tool call.\n\nTo use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning, -1 for end)\nand `new_str` (the text to insert).\n\n\n\nAdditional Shell Tool Instructions:\nExecute a command in the shell.\n\nThis will return the output and error concatenated into a single string, as\nyou would see from running on the command line. There will also be an indication\nof if the command succeeded or failed.\n\nAvoid commands that produce a large amount of output, and consider piping those outputs to files.\n\n**Important**: Each shell command runs in its own process. Things like directory changes or\nsourcing files do not persist between tool calls. So you may need to repeat them each time by\nstringing together commands.\nIf you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that\nthis tool does not run indefinitely.\n\n**Important**: Use ripgrep - `rg` - exclusively when you need to locate a file or a code reference,\nother solutions may produce too large output because of hidden files! For example *do not* use `find` or `ls -r`\n - List files by name: `rg --files | rg `\n - List files that contain a regex: `rg '' -l`\n\n - Multiple commands: Use && to chain commands, avoid newlines\n - Example: `cd example && ls` or `source env/bin/activate && pip install numpy`\n\n\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\nCloned Goose repo: /Users/angiej/workspace/goose\nMCP means Model Context Protocol. Docs: https://modelcontextprotocol.io/introduction\nUse GitHub CLI for GitHub-related tasks.\nWhen prompted for date-related information, do not rely on your internal knowledge for the current date. Instead, use the `date` terminal command to get the actual date and time.\nNEVER run blocking server commands (node server.js, npm start, etc.) - provide commands for user to run separately\n\n### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n# AGENTS Instructions\n\ngoose is an AI agent framework in Rust with CLI and Electron desktop interfaces.\n\n## Setup\n```bash\nsource bin/activate-hermit\ncargo build\n```\n\n## Commands\n\n### Build\n```bash\ncargo build # debug\ncargo build --release # release \njust release-binary # release + openapi\n```\n\n### Test\n```bash\ncargo test # all tests\ncargo test -p goose # specific crate\ncargo test --package goose --test mcp_integration_test\njust record-mcp-tests # record MCP\n```\n\n### Lint/Format\n```bash\ncargo fmt\n./scripts/clippy-lint.sh\ncargo clippy --fix\n```\n\n### UI\n```bash\njust generate-openapi # after server changes\njust run-ui # start desktop\ncd ui/desktop && npm test # test UI\n```\n\n## Structure\n```\ncrates/\n├── goose # core logic\n├── goose-bench # benchmarking\n├── goose-cli # CLI entry\n├── goose-server # backend (binary: goosed)\n├── goose-mcp # MCP extensions\n├── goose-test # test utilities\n├── mcp-client # MCP client\n├── mcp-core # MCP shared\n└── mcp-server # MCP server\n\ntemporal-service/ # Go scheduler\nui/desktop/ # Electron app\n```\n\n## Development Loop\n```bash\n# 1. source bin/activate-hermit\n# 2. Make changes\n# 3. cargo fmt\n# 4. cargo build\n# 5. cargo test -p \n# 6. ./scripts/clippy-lint.sh\n# 7. [if server] just generate-openapi\n```\n\n## Rules\n\nTest: Prefer tests/ folder, e.g. crates/goose/tests/\nError: Use anyhow::Result\nProvider: Implement Provider trait see providers/base.rs\nMCP: Extensions in crates/goose-mcp/\nServer: Changes need just generate-openapi\n\n## Never\n\nNever: Edit ui/desktop/openapi.json manually\nNever: Edit Cargo.toml use cargo add\nNever: Skip cargo fmt\nNever: Merge without ./scripts/clippy-lint.sh\n\n## Entry Points\n- CLI: crates/goose-cli/src/main.rs\n- Server: crates/goose-server/src/main.rs\n- UI: ui/desktop/src/main.ts\n- Agent: crates/goose/src/agents/agent.rs\n\nThis is a rust project with crates in the crates dir:\ngoose: the main code for goose, contains all the core logic\ngoose-bench: bench marking\ngoose-cli: the command line interface, use goose crate\ngoose-mcp: the mcp servers that ship with goose. the developer sub system is of special interest\ngoose-server: the server that suports the desktop (electron) app. also known as goosed\n\n\nui/desktop has an electron app in typescript. \n\nnon trivial features should be implemented in the goose crate and then be called from the goose-cli crate for the cli. for the desktop, you want to add routes to \ngoose-server/src/routes. you can then run `just generate-openapi` to generate the openapi spec which will modify the ui/desktop/src/api files. once you have\nthat you can call the functionality from the server from the typescript.\n\ntips: \n- can look at unstaged changes for what is being worked on if starting\n- always check rust compiles, cargo fmt etc and `./scripts/clippy-lint.sh` (as well as run tests in files you are working on)\n- in ui/desktop, look at how you can run lint checks and if other tests can run\n"}} STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} STDERR: 2025-09-27T04:13:30.418172Z  INFO rmcp::handler::server: client initialized STDERR: at /Users/angiej/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rmcp-0.6.2/src/handler/server.rs:218 STDERR: STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"text_editor","arguments":{"command":"view","path":"~/goose/crates/goose/tests/tmp/goose.txt"}}} -STDERR: 2025-09-27T04:13:30.418412Z  INFO rmcp::service: Service initialized as server, peer_info: Some(InitializeRequestParam { protocol_version: ProtocolVersion("2025-03-26"), capabilities: ClientCapabilities { experimental: None, roots: None, sampling: None, elicitation: None }, client_info: Implementation { name: "goose", version: "1.9.0" } }) +STDERR: 2025-09-27T04:13:30.418412Z  INFO rmcp::service: Service initialized as server, peer_info: Some(InitializeRequestParam { protocol_version: ProtocolVersion("2025-03-26"), capabilities: ClientCapabilities { experimental: None, roots: None, sampling: None, elicitation: None }, client_info: Implementation { name: "goose", version: "1.10.0" } }) STDERR: at /Users/angiej/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rmcp-0.6.2/src/service.rs:561 STDERR: in rmcp::service::serve_inner STDERR: diff --git a/crates/goose/tests/mcp_replays/github-mcp-serverstdio b/crates/goose/tests/mcp_replays/github-mcp-serverstdio index 07ac16ff8e11..8c36c8e0186b 100644 --- a/crates/goose/tests/mcp_replays/github-mcp-serverstdio +++ b/crates/goose/tests/mcp_replays/github-mcp-serverstdio @@ -1,4 +1,4 @@ -STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"1.9.0"}}} +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"1.10.0"}}} STDERR: GitHub MCP Server running on stdio STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"logging":{},"prompts":{},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"github-mcp-server","version":"version"}}} STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} diff --git a/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything index 7e2f44c87f53..f2d15c13ad67 100644 --- a/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything +++ b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything @@ -1,4 +1,4 @@ -STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"1.9.0"}}} +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"1.10.0"}}} STDERR: 2025-09-26 23:13:04 - Starting npx setup script. STDERR: 2025-09-26 23:13:04 - Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist. STDERR: 2025-09-26 23:13:04 - Changing to directory ~/.config/goose/mcp-hermit. diff --git a/crates/goose/tests/mcp_replays/uvxmcp-server-fetch b/crates/goose/tests/mcp_replays/uvxmcp-server-fetch index 411be2b02413..c6749e12d3a2 100644 --- a/crates/goose/tests/mcp_replays/uvxmcp-server-fetch +++ b/crates/goose/tests/mcp_replays/uvxmcp-server-fetch @@ -1,4 +1,4 @@ -STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"1.9.0"}}} +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"1.10.0"}}} STDERR: 2025-09-26 23:13:04 - Starting uvx setup script. STDERR: 2025-09-26 23:13:04 - Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist. STDERR: 2025-09-26 23:13:04 - Changing to directory ~/.config/goose/mcp-hermit. diff --git a/download_cli.sh b/download_cli.sh index 610ca081fc9b..30a142ef5bff 100755 --- a/download_cli.sh +++ b/download_cli.sh @@ -37,7 +37,7 @@ if ! command -v tar >/dev/null 2>&1 && ! command -v unzip >/dev/null 2>&1; then fi # Check for required extraction tools based on detected OS -if [ "$OS" = "windows" ]; then +if [ "${OS:-}" = "windows" ]; then # Windows uses PowerShell's built-in Expand-Archive - check if PowerShell is available if ! command -v powershell.exe >/dev/null 2>&1 && ! command -v pwsh >/dev/null 2>&1; then echo "Warning: PowerShell is recommended to extract Windows packages but was not found." @@ -45,7 +45,7 @@ if [ "$OS" = "windows" ]; then fi else if ! command -v tar >/dev/null 2>&1; then - echo "Error: 'tar' is required to extract packages for $OS. Please install tar and try again." + echo "Error: 'tar' is required to extract packages for ${OS:-unknown}. Please install tar and try again." exit 1 fi fi diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index a53946407b46..e43d0e93dd54 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "1.9.0" + "version": "1.10.0" }, "paths": { "/agent/add_sub_recipes": { diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index ab4fcbb48ef6..f712872f6256 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "goose-app", - "version": "1.9.0", + "version": "1.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goose-app", - "version": "1.9.0", + "version": "1.10.2", "license": "Apache-2.0", "dependencies": { "@ai-sdk/openai": "^2.0.14", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index e42f1bae3851..6c15a3bf3b8b 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -1,7 +1,7 @@ { "name": "goose-app", "productName": "goose", - "version": "1.9.0", + "version": "1.10.2", "description": "goose App", "engines": { "node": "^22.17.1" diff --git a/ui/desktop/src/bin/node b/ui/desktop/src/bin/node new file mode 100755 index 000000000000..9ba2327c7cda --- /dev/null +++ b/ui/desktop/src/bin/node @@ -0,0 +1,16 @@ +#!/bin/bash + +# Enable strict mode to exit on errors and unset variables +set -euo pipefail + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source the common setup script +source "$SCRIPT_DIR/node-setup-common.sh" + +# Final step: Execute node with passed arguments +log "Executing 'node' command with arguments: $*" +node "$@" || log "Failed to execute 'node' with arguments: $*" + +log "node script completed successfully." diff --git a/ui/desktop/src/bin/node-setup-common.sh b/ui/desktop/src/bin/node-setup-common.sh new file mode 100755 index 000000000000..97c3551d5ac0 --- /dev/null +++ b/ui/desktop/src/bin/node-setup-common.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Common setup script for node and npx +# This script sets up hermit and node.js environment + +# Enable strict mode to exit on errors and unset variables +set -euo pipefail + +# Set log file +LOG_FILE="/tmp/mcp.log" + +# Clear the log file at the start +> "$LOG_FILE" + +# Function for logging +log() { + local MESSAGE="$1" + echo "$(date +'%Y-%m-%d %H:%M:%S') - $MESSAGE" | tee -a "$LOG_FILE" >&2 +} + +# Trap errors and log them before exiting +trap 'log "An error occurred. Exiting with status $?."' ERR + +log "Starting node setup (common)." + +# Ensure ~/.config/goose/mcp-hermit/bin exists +log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist." +mkdir -p ~/.config/goose/mcp-hermit/bin + +# Change to the ~/.config/goose/mcp-hermit directory +log "Changing to directory ~/.config/goose/mcp-hermit." +cd ~/.config/goose/mcp-hermit + + +# Check if hermit binary exists and download if not +if [ ! -f ~/.config/goose/mcp-hermit/bin/hermit ]; then + log "Hermit binary not found. Downloading hermit binary." + curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \ + | gzip -dc > ~/.config/goose/mcp-hermit/bin/hermit && chmod +x ~/.config/goose/mcp-hermit/bin/hermit + log "Hermit binary downloaded and made executable." +else + log "Hermit binary already exists. Skipping download." +fi + + +log "setting hermit cache to be local for MCP servers" +mkdir -p ~/.config/goose/mcp-hermit/cache +export HERMIT_STATE_DIR=~/.config/goose/mcp-hermit/cache + + +# Update PATH +export PATH=~/.config/goose/mcp-hermit/bin:$PATH +log "Updated PATH to include ~/.config/goose/mcp-hermit/bin." + + +# Verify hermit installation +log "Checking for hermit in PATH." +which hermit >> "$LOG_FILE" + +# Initialize hermit +log "Initializing hermit." +hermit init >> "$LOG_FILE" + +# Install Node.js using hermit +log "Installing Node.js with hermit." +hermit install node >> "$LOG_FILE" + +# Verify installations +log "Verifying installation locations:" +log "hermit: $(which hermit)" +log "node: $(which node)" +log "npx: $(which npx)" + + +log "Checking for GOOSE_NPM_REGISTRY and GOOSE_NPM_CERT environment variables for custom npm registry setup..." +# Check if GOOSE_NPM_REGISTRY is set and accessible +if [ -n "${GOOSE_NPM_REGISTRY:-}" ] && curl -s --head --fail "$GOOSE_NPM_REGISTRY" > /dev/null; then + log "Checking custom goose registry availability: $GOOSE_NPM_REGISTRY" + log "$GOOSE_NPM_REGISTRY is accessible. Using it for npm registry." + export NPM_CONFIG_REGISTRY="$GOOSE_NPM_REGISTRY" + + # Check if GOOSE_NPM_CERT is set and accessible + if [ -n "${GOOSE_NPM_CERT:-}" ] && curl -s --head --fail "$GOOSE_NPM_CERT" > /dev/null; then + log "Downloading certificate from: $GOOSE_NPM_CERT" + curl -sSL -o ~/.config/goose/mcp-hermit/cert.pem "$GOOSE_NPM_CERT" + if [ $? -eq 0 ]; then + log "Certificate downloaded successfully." + export NODE_EXTRA_CA_CERTS=~/.config/goose/mcp-hermit/cert.pem + else + log "Unable to download the certificate. Skipping certificate setup." + fi + else + log "GOOSE_NPM_CERT is either not set or not accessible. Skipping certificate setup." + fi + +else + log "GOOSE_NPM_REGISTRY is either not set or not accessible. Falling back to default npm registry." + export NPM_CONFIG_REGISTRY="https://registry.npmjs.org/" +fi + +log "Node setup (common) completed successfully." diff --git a/ui/desktop/src/bin/npx b/ui/desktop/src/bin/npx index 92c361e55c65..7e6228dee5b5 100755 --- a/ui/desktop/src/bin/npx +++ b/ui/desktop/src/bin/npx @@ -3,103 +3,14 @@ # Enable strict mode to exit on errors and unset variables set -euo pipefail -# Set log file -LOG_FILE="/tmp/mcp.log" - -# Clear the log file at the start -> "$LOG_FILE" - -# Function for logging -log() { - local MESSAGE="$1" - echo "$(date +'%Y-%m-%d %H:%M:%S') - $MESSAGE" | tee -a "$LOG_FILE" >&2 -} - -# Trap errors and log them before exiting -trap 'log "An error occurred. Exiting with status $?."' ERR - -log "Starting npx setup script." - -# Ensure ~/.config/goose/mcp-hermit/bin exists -log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist." -mkdir -p ~/.config/goose/mcp-hermit/bin - -# Change to the ~/.config/goose/mcp-hermit directory -log "Changing to directory ~/.config/goose/mcp-hermit." -cd ~/.config/goose/mcp-hermit - - -# Check if hermit binary exists and download if not -if [ ! -f ~/.config/goose/mcp-hermit/bin/hermit ]; then - log "Hermit binary not found. Downloading hermit binary." - curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \ - | gzip -dc > ~/.config/goose/mcp-hermit/bin/hermit && chmod +x ~/.config/goose/mcp-hermit/bin/hermit - log "Hermit binary downloaded and made executable." -else - log "Hermit binary already exists. Skipping download." -fi - - -log "setting hermit cache to be local for MCP servers" -mkdir -p ~/.config/goose/mcp-hermit/cache -export HERMIT_STATE_DIR=~/.config/goose/mcp-hermit/cache - - -# Update PATH -export PATH=~/.config/goose/mcp-hermit/bin:$PATH -log "Updated PATH to include ~/.config/goose/mcp-hermit/bin." - - -# Verify hermit installation -log "Checking for hermit in PATH." -which hermit >> "$LOG_FILE" - -# Initialize hermit -log "Initializing hermit." -hermit init >> "$LOG_FILE" - -# Install Node.js using hermit -log "Installing Node.js with hermit." -hermit install node >> "$LOG_FILE" - -# Verify installations -log "Verifying installation locations:" -log "hermit: $(which hermit)" -log "node: $(which node)" -log "npx: $(which npx)" - - -log "Checking for GOOSE_NPM_REGISTRY and GOOSE_NPM_CERT environment variables for custom npm registry setup..." -# Check if GOOSE_NPM_REGISTRY is set and accessible -if [ -n "${GOOSE_NPM_REGISTRY:-}" ] && curl -s --head --fail "$GOOSE_NPM_REGISTRY" > /dev/null; then - log "Checking custom goose registry availability: $GOOSE_NPM_REGISTRY" - log "$GOOSE_NPM_REGISTRY is accessible. Using it for npm registry." - export NPM_CONFIG_REGISTRY="$GOOSE_NPM_REGISTRY" - - # Check if GOOSE_NPM_CERT is set and accessible - if [ -n "${GOOSE_NPM_CERT:-}" ] && curl -s --head --fail "$GOOSE_NPM_CERT" > /dev/null; then - log "Downloading certificate from: $GOOSE_NPM_CERT" - curl -sSL -o ~/.config/goose/mcp-hermit/cert.pem "$GOOSE_NPM_CERT" - if [ $? -eq 0 ]; then - log "Certificate downloaded successfully." - export NODE_EXTRA_CA_CERTS=~/.config/goose/mcp-hermit/cert.pem - else - log "Unable to download the certificate. Skipping certificate setup." - fi - else - log "GOOSE_NPM_CERT is either not set or not accessible. Skipping certificate setup." - fi - -else - log "GOOSE_NPM_REGISTRY is either not set or not accessible. Falling back to default npm registry." - export NPM_CONFIG_REGISTRY="https://registry.npmjs.org/" -fi - - +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Source the common setup script +source "$SCRIPT_DIR/node-setup-common.sh" # Final step: Execute npx with passed arguments log "Executing 'npx' command with arguments: $*" npx "$@" || log "Failed to execute 'npx' with arguments: $*" -log "npx setup script completed successfully." +log "npx script completed successfully." diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 4f7d6a2cba63..3ddc9465f71a 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1184,9 +1184,6 @@ export default function ChatInput({ !agentIsReady || isExtensionsLoading; - const isUserInputDisabled = - isAnyImageLoading || isAnyDroppedFileLoading || isRecording || isTranscribing || isCompacting; - // Queue management functions - no storage persistence, only in-memory const handleRemoveQueuedMessage = (messageId: string) => { setQueuedMessages((prev) => prev.filter((msg) => msg.id !== messageId)); @@ -1301,7 +1298,6 @@ export default function ChatInput({ onBlur={() => setIsFocused(false)} ref={textAreaRef} rows={1} - disabled={isUserInputDisabled} style={{ maxHeight: `${maxHeight}px`, overflowY: 'auto', diff --git a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx index 553c04f763b5..dc168748e676 100644 --- a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx +++ b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx @@ -38,6 +38,16 @@ export function RecipeFormFields({ const [newParameterName, setNewParameterName] = useState(''); const [expandedParameters, setExpandedParameters] = useState>(new Set()); + // Force re-render when instructions, prompt, or activities change + const [_forceRender, setForceRender] = useState(0); + + React.useEffect(() => { + return form.store.subscribe(() => { + // Force re-render when any form field changes to update parameter usage indicators + setForceRender((prev) => prev + 1); + }); + }, [form.store]); + const parseParametersFromInstructions = React.useCallback( (instructions: string, prompt?: string, activities?: string[]): Parameter[] => { const instructionVars = extractTemplateVariables(instructions); diff --git a/ui/desktop/src/hooks/useRecipeManager.ts b/ui/desktop/src/hooks/useRecipeManager.ts index 5abf9f47df29..07cc8723c6f4 100644 --- a/ui/desktop/src/hooks/useRecipeManager.ts +++ b/ui/desktop/src/hooks/useRecipeManager.ts @@ -26,6 +26,7 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => { const messagesRef = useRef(messages); const isCreatingRecipeRef = useRef(false); + const hasCheckedRecipeRef = useRef(false); useEffect(() => { messagesRef.current = messages; @@ -54,6 +55,7 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => { setRecipeAccepted(false); setIsParameterModalOpen(false); setIsRecipeWarningModalOpen(false); + hasCheckedRecipeRef.current = false; // Reset check flag for new recipe chatContext.setChat({ ...chatContext.chat, @@ -75,14 +77,23 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => { useEffect(() => { const checkRecipeAcceptance = async () => { + // Only check once per recipe load + if (hasCheckedRecipeRef.current) { + return; + } + if (finalRecipe) { + hasCheckedRecipeRef.current = true; + // If the recipe comes from session metadata (not from navigation state), // it means it was already accepted in a previous session, so auto-accept it - const isFromSessionMetadata = !recipe && finalRecipe; + const hasMessages = chat.messages.length > 0; + const isFromSessionMetadata = !recipe && finalRecipe && hasMessages; if (isFromSessionMetadata) { // Recipe loaded from session metadata should be automatically accepted setRecipeAccepted(true); + setIsRecipeWarningModalOpen(false); return; } @@ -108,7 +119,7 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => { }; checkRecipeAcceptance(); - }, [finalRecipe, recipe]); + }, [finalRecipe, recipe, chat.messages.length]); // Filter parameters to only show valid ones that are actually used in the recipe const filteredParameters = useMemo(() => { diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index eaeb980e38f7..31551780d91f 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -345,6 +345,7 @@ app.on('open-url', async (_event, url) => { recipeDeeplink || undefined, scheduledJobId || undefined ); + windowDeeplinkURL = null; return; // Skip the rest of the handler } @@ -365,6 +366,7 @@ app.on('open-url', async (_event, url) => { } else if (parsedUrl.hostname === 'sessions') { firstOpenWindow.webContents.send('open-shared-session', pendingDeepLink); } + pendingDeepLink = null; } });