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::*;