mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: dedicated goal DB (#23300)
## Why Thread goals are moving toward extension-owned runtime behavior, but their persisted state was still stored in the shared state database. This makes the goal store harder to isolate and keeps future storage splits tied to ad hoc runtime plumbing. This PR gives goals their own SQLite database while keeping the existing `StateRuntime` entry point. The goal is to make this the pattern for adding more dedicated runtime databases later. This also reduce load on existing DB and reduce contention ## Limitation Thread preview from goal is not supported anymore. I'm looking into this [EDIT]: solved ## What changed - Added a dedicated `goals_1.sqlite` database with its own `goals_migrations` directory. - Moved `thread_goals` creation into the goals DB migration set. - Dropped the old `thread_goals` table from the main state DB with a normal state migration. There is intentionally no backfill for existing goal rows. - Changed `GoalStore` to be backed only by the goals DB pool. - Removed the old goal-write side effect that filled empty `threads.preview` values from the goal objective. - Added shared runtime DB path metadata so startup, telemetry, `codex doctor`, and repair handling can include future DBs without bespoke path lists. - Updated Bazel compile data so the new goals migration directory is available to `sqlx::migrate!`. ## Verification - `cargo check --tests -p codex-state -p codex-cli -p codex-core -p codex-app-server` - `just fix -p codex-state` - `just fix -p codex-cli` - `just fix -p codex-app-server`
This commit is contained in:
@@ -23,6 +23,7 @@ use codex_otel::GOAL_DURATION_SECONDS_METRIC;
|
||||
use codex_otel::GOAL_RESUMED_METRIC;
|
||||
use codex_otel::GOAL_TOKEN_COUNT_METRIC;
|
||||
use codex_otel::GOAL_USAGE_LIMITED_METRIC;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
@@ -530,6 +531,14 @@ impl Session {
|
||||
})?
|
||||
};
|
||||
|
||||
if objective.is_some() {
|
||||
set_thread_preview_from_goal_objective(
|
||||
&state_db,
|
||||
self.conversation_id,
|
||||
goal.objective.as_str(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let goal_status = goal.status;
|
||||
let goal_id = goal.goal_id.clone();
|
||||
let previous_status_for_goal = if replacing_goal {
|
||||
@@ -611,6 +620,12 @@ impl Session {
|
||||
)
|
||||
})?;
|
||||
|
||||
set_thread_preview_from_goal_objective(
|
||||
&state_db,
|
||||
self.conversation_id,
|
||||
goal.objective.as_str(),
|
||||
)
|
||||
.await;
|
||||
let goal_id = goal.goal_id.clone();
|
||||
self.emit_goal_created_metric();
|
||||
let goal = protocol_goal_from_state(goal);
|
||||
@@ -1508,6 +1523,21 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_thread_preview_from_goal_objective(
|
||||
state_db: &StateDbHandle,
|
||||
thread_id: ThreadId,
|
||||
objective: &str,
|
||||
) {
|
||||
if let Err(err) = state_db
|
||||
.set_thread_preview_if_empty(thread_id, objective)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"failed to set empty thread preview from goal objective for {thread_id}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn should_ignore_goal_for_mode(mode: ModeKind) -> bool {
|
||||
mode == ModeKind::Plan
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use tracing::Span;
|
||||
|
||||
use crate::goals::CreateGoalRequest;
|
||||
use crate::goals::ExternalGoalPreviousStatus;
|
||||
use crate::goals::ExternalGoalSet;
|
||||
use crate::goals::GoalRuntimeEvent;
|
||||
@@ -8415,6 +8416,66 @@ async fn goal_test_state_db(sess: &Session) -> anyhow::Result<crate::StateDbHand
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_thread_goal_fills_empty_thread_preview() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let state_db = goal_test_state_db(sess.as_ref()).await?;
|
||||
|
||||
let page = state_db
|
||||
.list_threads(
|
||||
/*page_size*/ 10,
|
||||
codex_state::ThreadFilterOptions {
|
||||
archived_only: false,
|
||||
allowed_sources: &[],
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
anchor: None,
|
||||
sort_key: codex_state::SortKey::UpdatedAt,
|
||||
sort_direction: codex_state::SortDirection::Desc,
|
||||
search_term: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
assert!(page.items.is_empty());
|
||||
|
||||
sess.create_thread_goal(
|
||||
tc.as_ref(),
|
||||
CreateGoalRequest {
|
||||
objective: "Keep improving the benchmark".to_string(),
|
||||
token_budget: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let page = state_db
|
||||
.list_threads(
|
||||
/*page_size*/ 10,
|
||||
codex_state::ThreadFilterOptions {
|
||||
archived_only: false,
|
||||
allowed_sources: &[],
|
||||
model_providers: None,
|
||||
cwd_filters: None,
|
||||
anchor: None,
|
||||
sort_key: codex_state::SortKey::UpdatedAt,
|
||||
sort_direction: codex_state::SortDirection::Desc,
|
||||
search_term: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let ids = page
|
||||
.items
|
||||
.iter()
|
||||
.map(|thread| thread.id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(vec![sess.conversation_id], ids);
|
||||
assert_eq!(
|
||||
Some("Keep improving the benchmark"),
|
||||
page.items[0].preview.as_deref()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn budget_limited_accounting_steers_active_turn_without_aborting() -> anyhow::Result<()> {
|
||||
let (sess, tc, rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
|
||||
Reference in New Issue
Block a user