From 27980893bd034ebf8d6d8ba4191d77c7d56a28b7 Mon Sep 17 00:00:00 2001 From: Benjamin Riley Zimmerman Date: Mon, 13 Apr 2026 23:57:37 -0700 Subject: [PATCH] . --- Cargo.lock | 3 +++ crates/forge_api/src/forge_api.rs | 14 -------------- crates/forge_main/Cargo.toml | 3 +++ crates/forge_main/src/main.rs | 22 +++++++++++++++++++++- crates/forge_main/src/ui.rs | 10 +++++----- crates/forge_repo/src/database/pool.rs | 1 - crates/forge_repo/src/forge_repo.rs | 14 +++++++++----- crates/forge_repo/src/lib.rs | 3 +++ 8 files changed, 44 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0eddc13a2a..7fe8c5cb07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2297,8 +2297,11 @@ dependencies = [ "forge_domain", "forge_embed", "forge_fs", + "forge_infra", "forge_markdown_stream", + "forge_repo", "forge_select", + "forge_services", "forge_spinner", "forge_tracker", "forge_walker", diff --git a/crates/forge_api/src/forge_api.rs b/crates/forge_api/src/forge_api.rs index 1372a314ec..b415d3b576 100644 --- a/crates/forge_api/src/forge_api.rs +++ b/crates/forge_api/src/forge_api.rs @@ -10,7 +10,6 @@ use forge_app::{ FileDiscoveryService, ForgeApp, GitApp, GrpcInfra, McpConfigManager, McpService, ProviderAuthService, ProviderService, Services, User, UserUsage, Walker, WorkspaceService, }; -use forge_config::ForgeConfig; use forge_domain::{Agent, ConsoleWriter, *}; use forge_infra::ForgeInfra; use forge_repo::ForgeRepo; @@ -42,19 +41,6 @@ impl ForgeAPI { } impl ForgeAPI>, ForgeRepo> { - /// Creates a fully-initialized [`ForgeAPI`] from a pre-read configuration. - /// - /// # Arguments - /// * `cwd` - The working directory path for environment and file resolution - /// * `config` - Pre-read application configuration (from startup) - /// * `services_url` - Pre-validated URL for the gRPC workspace server - pub fn init(cwd: PathBuf, config: ForgeConfig) -> Self { - let infra = Arc::new(ForgeInfra::new(cwd, config)); - let repo = Arc::new(ForgeRepo::new(infra.clone())); - let app = Arc::new(ForgeServices::new(repo.clone())); - ForgeAPI::new(app, repo) - } - pub async fn get_skills_internal(&self) -> Result> { use forge_domain::SkillRepository; self.infra.load_skills().await diff --git a/crates/forge_main/Cargo.toml b/crates/forge_main/Cargo.toml index ad29d5bd66..18b8f2c01d 100644 --- a/crates/forge_main/Cargo.toml +++ b/crates/forge_main/Cargo.toml @@ -23,6 +23,9 @@ forge_display.workspace = true forge_tracker.workspace = true forge_spinner.workspace = true +forge_repo.workspace = true +forge_infra.workspace = true +forge_services.workspace = true forge_select.workspace = true merge.workspace = true diff --git a/crates/forge_main/src/main.rs b/crates/forge_main/src/main.rs index 0b68b7e30b..f731afc273 100644 --- a/crates/forge_main/src/main.rs +++ b/crates/forge_main/src/main.rs @@ -1,12 +1,15 @@ use std::io::Read; use std::panic; use std::path::PathBuf; +use std::sync::Arc; use anyhow::{Context, Result}; use clap::Parser; use forge_api::ForgeAPI; +use forge_app::EnvironmentInfra; use forge_config::ForgeConfig; use forge_domain::TitleFormat; +use forge_infra::ForgeInfra; use forge_main::{Cli, Sandbox, TitleDisplayExt, UI, tracker}; /// Enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on the stdout console handle. @@ -120,8 +123,25 @@ async fn run() -> Result<()> { (_, _) => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), }; + // ForgeInfra is created once and reused across /new — it owns long-lived + // resources (HTTP client, gRPC) that don't need to reset per conversation. + let infra = Arc::new(ForgeInfra::new(cwd.clone(), config.clone())); + let mut ui = UI::init(cli, config, move |config| { - ForgeAPI::init(cwd.clone(), config) + // Config is intentionally unused here — ForgeInfra is frozen at startup. + let _ = config; + + // Fresh pool on every /new. SQLite's busy_timeout handles any brief + // contention while the old pool drains from lingering hydration tasks. + let db_pool = Arc::new( + forge_repo::DatabasePool::try_from(forge_repo::PoolConfig::new( + infra.get_environment().database_path(), + )) + .context("Failed to open Forge database")?, + ); + let repo = Arc::new(forge_repo::ForgeRepo::new(infra.clone(), db_pool)); + let services = Arc::new(forge_services::ForgeServices::new(repo.clone())); + Ok(ForgeAPI::new(services, repo)) })?; ui.run().await; diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 76f7739213..eca32352a8 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -101,10 +101,10 @@ fn format_mcp_headers(server: &forge_domain::McpServerConfig) -> Option } } -pub struct UI A> { +pub struct UI anyhow::Result> { markdown: MarkdownFormat, state: UIState, - api: Arc, + api: Arc, new_api: Arc, console: Console, command: Arc, @@ -115,7 +115,7 @@ pub struct UI A> { _guard: forge_tracker::Guard, } -impl A + Send + Sync> UI { +impl anyhow::Result + Send + Sync> UI { /// Writes a line to the console output /// Takes anything that implements ToString trait fn writeln(&mut self, content: T) -> anyhow::Result<()> { @@ -161,7 +161,7 @@ impl A + Send + Sync> UI async fn on_new(&mut self) -> Result<()> { let config = forge_config::ForgeConfig::read().unwrap_or_default(); self.config = config.clone(); - self.api = Arc::new((self.new_api)(config)); + self.api = Arc::new((self.new_api)(config)?); self.init_state(false).await?; // Set agent if provided via CLI @@ -216,7 +216,7 @@ impl A + Send + Sync> UI /// from `forge config set` are reflected in new conversations pub fn init(cli: Cli, config: ForgeConfig, f: F) -> Result { // Parse CLI arguments first to get flags - let api = Arc::new(f(config.clone())); + let api = Arc::new(f(config.clone())?); let env = api.environment(); let command = Arc::new(ForgeCommandManager::default()); let spinner = SharedSpinner::new(SpinnerManager::new(api.clone())); diff --git a/crates/forge_repo/src/database/pool.rs b/crates/forge_repo/src/database/pool.rs index 3abae19965..8379de4923 100644 --- a/crates/forge_repo/src/database/pool.rs +++ b/crates/forge_repo/src/database/pool.rs @@ -1,4 +1,3 @@ -#![allow(dead_code)] use std::path::PathBuf; use std::time::Duration; diff --git a/crates/forge_repo/src/forge_repo.rs b/crates/forge_repo/src/forge_repo.rs index 34d1bb8498..975a2be7eb 100644 --- a/crates/forge_repo/src/forge_repo.rs +++ b/crates/forge_repo/src/forge_repo.rs @@ -26,7 +26,7 @@ use url::Url; use crate::agent::ForgeAgentRepository; use crate::context_engine::ForgeContextEngineRepository; use crate::conversation::ConversationRepositoryImpl; -use crate::database::{DatabasePool, PoolConfig}; +use crate::database::DatabasePool; use crate::fs_snap::ForgeFileSnapshotService; use crate::fuzzy_search::ForgeFuzzySearchRepository; use crate::provider::{ForgeChatRepository, ForgeProviderRepository}; @@ -60,13 +60,17 @@ impl< + HttpInfra, > ForgeRepo { - pub fn new(infra: Arc) -> Self { + /// Creates a new [`ForgeRepo`] with the provided infrastructure and a + /// shared database pool. + /// + /// The pool is created once at application startup and reused across + /// `/new` conversation resets so that a fresh `ForgeRepo` never races + /// with the previous instance for the same SQLite file. + pub fn new(infra: Arc, db_pool: Arc) -> Self { let env = infra.get_environment(); let file_snapshot_service = Arc::new(ForgeFileSnapshotService::new(env.clone())); - let db_pool = - Arc::new(DatabasePool::try_from(PoolConfig::new(env.database_path())).unwrap()); let conversation_repository = Arc::new(ConversationRepositoryImpl::new( - db_pool.clone(), + db_pool, env.workspace_hash(), )); diff --git a/crates/forge_repo/src/lib.rs b/crates/forge_repo/src/lib.rs index d489072371..b0ae5ee2a8 100644 --- a/crates/forge_repo/src/lib.rs +++ b/crates/forge_repo/src/lib.rs @@ -14,5 +14,8 @@ mod proto_generated { tonic::include_proto!("forge.v1"); } +// Expose the database pool types so callers (e.g. forge_api) can construct +// and share a pool without depending on internal module paths. +pub use database::{DatabasePool, PoolConfig}; // Only expose forge_repo container pub use forge_repo::*;