mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
f10ddc3f13
Fixes #20792 ## Why `/goal`-first threads are valid resumable threads, but they can be missing from `codex resume` and app recents because discovery depends on metadata derived from a normal first user message. PR #21489 attempted to fix this by using the goal objective as `first_user_message`. Review feedback pointed out that `first_user_message` does more than provide visible text today: it gates listing, supplies preview text, and participates in deciding whether a later title should surface as a distinct thread name. Reusing it for the goal objective could leave a `/goal`-first thread with `first_user_message=<goal>` and `title=<later prompt>`, even though the goal should only provide the initial visible preview. This PR follows that feedback by and keeps the `first_user_message` as is but introduces a new `preview` field to separate concerns. The `preview` field is populated from the first user message or the goal objective. We can extend it in the future to include other sources. ## What Changed - Added internal thread `preview` metadata in `codex-state`, including a SQLite migration that backfills from `first_user_message` and from existing `thread_goals` objectives when needed. - Treated `ThreadGoalUpdated` as preview-bearing metadata so goal-first threads can be listed and searched without mutating `first_user_message`. - Updated rollout listing, state queries, thread-store conversion, and app-server mapping to use preview metadata while continuing to expose the existing public `preview` field. - Preserved title/name distinctness behavior around literal `first_user_message`, so a later normal prompt after `/goal` does not surface as a separate name just because the goal supplied the initial preview. - Preserved compatibility for older/internal metadata writes by deriving preview from `first_user_message` when explicit preview metadata is absent. ## Verification - Manually verified that a thread that starts with a `/goal <objective>` shows up in the resume picker.
582 lines
20 KiB
Rust
582 lines
20 KiB
Rust
use anyhow::Result;
|
|
use chrono::DateTime;
|
|
use chrono::Utc;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::ThreadSource;
|
|
use sqlx::Row;
|
|
use sqlx::sqlite::SqliteRow;
|
|
use std::path::PathBuf;
|
|
|
|
/// The sort key to use when listing threads.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SortKey {
|
|
/// Sort by the thread's creation timestamp.
|
|
CreatedAt,
|
|
/// Sort by the thread's last update timestamp.
|
|
UpdatedAt,
|
|
}
|
|
|
|
/// Sort direction to use when listing threads.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SortDirection {
|
|
Asc,
|
|
Desc,
|
|
}
|
|
|
|
/// A pagination anchor used for keyset pagination.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Anchor {
|
|
/// The timestamp component of the anchor.
|
|
pub ts: DateTime<Utc>,
|
|
}
|
|
|
|
/// A single page of thread metadata results.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ThreadsPage {
|
|
/// The thread metadata items in this page.
|
|
pub items: Vec<ThreadMetadata>,
|
|
/// The next anchor to use for pagination, if any.
|
|
pub next_anchor: Option<Anchor>,
|
|
/// The number of rows scanned to produce this page.
|
|
pub num_scanned_rows: usize,
|
|
}
|
|
|
|
/// The outcome of extracting metadata from a rollout.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ExtractionOutcome {
|
|
/// The extracted thread metadata.
|
|
pub metadata: ThreadMetadata,
|
|
/// The explicit thread memory mode from rollout metadata, if present.
|
|
pub memory_mode: Option<String>,
|
|
/// The number of rollout lines that failed to parse.
|
|
pub parse_errors: usize,
|
|
}
|
|
|
|
/// Canonical thread metadata derived from rollout files.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ThreadMetadata {
|
|
/// The thread identifier.
|
|
pub id: ThreadId,
|
|
/// The absolute rollout path on disk.
|
|
pub rollout_path: PathBuf,
|
|
/// The creation timestamp.
|
|
pub created_at: DateTime<Utc>,
|
|
/// The last update timestamp.
|
|
pub updated_at: DateTime<Utc>,
|
|
/// The session source (stringified enum).
|
|
pub source: String,
|
|
/// Optional analytics source classification for this thread.
|
|
pub thread_source: Option<ThreadSource>,
|
|
/// Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
|
pub agent_nickname: Option<String>,
|
|
/// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
|
pub agent_role: Option<String>,
|
|
/// Optional canonical agent path assigned to an AgentControl-spawned sub-agent.
|
|
pub agent_path: Option<String>,
|
|
/// The model provider identifier.
|
|
pub model_provider: String,
|
|
/// The latest observed model for the thread.
|
|
pub model: Option<String>,
|
|
/// The latest observed reasoning effort for the thread.
|
|
pub reasoning_effort: Option<ReasoningEffort>,
|
|
/// The working directory for the thread.
|
|
pub cwd: PathBuf,
|
|
/// Version of the CLI that created the thread.
|
|
pub cli_version: String,
|
|
/// A best-effort thread title.
|
|
pub title: String,
|
|
/// Best available user-facing preview for discovery and list display.
|
|
pub preview: Option<String>,
|
|
/// The sandbox policy (stringified enum).
|
|
pub sandbox_policy: String,
|
|
/// The approval mode (stringified enum).
|
|
pub approval_mode: String,
|
|
/// The last observed token usage.
|
|
pub tokens_used: i64,
|
|
/// First user message observed for this thread, if any.
|
|
pub first_user_message: Option<String>,
|
|
/// The archive timestamp, if the thread is archived.
|
|
pub archived_at: Option<DateTime<Utc>>,
|
|
/// The git commit SHA, if known.
|
|
pub git_sha: Option<String>,
|
|
/// The git branch name, if known.
|
|
pub git_branch: Option<String>,
|
|
/// The git origin URL, if known.
|
|
pub git_origin_url: Option<String>,
|
|
}
|
|
|
|
/// Builder data required to construct [`ThreadMetadata`] without parsing filenames.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ThreadMetadataBuilder {
|
|
/// The thread identifier.
|
|
pub id: ThreadId,
|
|
/// The absolute rollout path on disk.
|
|
pub rollout_path: PathBuf,
|
|
/// The creation timestamp.
|
|
pub created_at: DateTime<Utc>,
|
|
/// The last update timestamp, if known.
|
|
pub updated_at: Option<DateTime<Utc>>,
|
|
/// The session source.
|
|
pub source: SessionSource,
|
|
/// Optional analytics source classification for this thread.
|
|
pub thread_source: Option<ThreadSource>,
|
|
/// Optional random unique nickname assigned to the session.
|
|
pub agent_nickname: Option<String>,
|
|
/// Optional role (agent_role) assigned to the session.
|
|
pub agent_role: Option<String>,
|
|
/// Optional canonical agent path assigned to the session.
|
|
pub agent_path: Option<String>,
|
|
/// The model provider identifier, if known.
|
|
pub model_provider: Option<String>,
|
|
/// The working directory for the thread.
|
|
pub cwd: PathBuf,
|
|
/// Version of the CLI that created the thread.
|
|
pub cli_version: Option<String>,
|
|
/// The sandbox policy.
|
|
pub sandbox_policy: SandboxPolicy,
|
|
/// The approval mode.
|
|
pub approval_mode: AskForApproval,
|
|
/// The archive timestamp, if the thread is archived.
|
|
pub archived_at: Option<DateTime<Utc>>,
|
|
/// The git commit SHA, if known.
|
|
pub git_sha: Option<String>,
|
|
/// The git branch name, if known.
|
|
pub git_branch: Option<String>,
|
|
/// The git origin URL, if known.
|
|
pub git_origin_url: Option<String>,
|
|
}
|
|
|
|
impl ThreadMetadataBuilder {
|
|
/// Create a new builder with required fields and sensible defaults.
|
|
pub fn new(
|
|
id: ThreadId,
|
|
rollout_path: PathBuf,
|
|
created_at: DateTime<Utc>,
|
|
source: SessionSource,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
rollout_path,
|
|
created_at,
|
|
updated_at: None,
|
|
source,
|
|
thread_source: None,
|
|
agent_nickname: None,
|
|
agent_role: None,
|
|
agent_path: None,
|
|
model_provider: None,
|
|
cwd: PathBuf::new(),
|
|
cli_version: None,
|
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
|
approval_mode: AskForApproval::OnRequest,
|
|
archived_at: None,
|
|
git_sha: None,
|
|
git_branch: None,
|
|
git_origin_url: None,
|
|
}
|
|
}
|
|
|
|
/// Build canonical thread metadata, filling missing values from defaults.
|
|
pub fn build(&self, default_provider: &str) -> ThreadMetadata {
|
|
let source = crate::extract::enum_to_string(&self.source);
|
|
let sandbox_policy = crate::extract::enum_to_string(&self.sandbox_policy);
|
|
let approval_mode = crate::extract::enum_to_string(&self.approval_mode);
|
|
let created_at = canonicalize_datetime(self.created_at);
|
|
let updated_at = self
|
|
.updated_at
|
|
.map(canonicalize_datetime)
|
|
.unwrap_or(created_at);
|
|
ThreadMetadata {
|
|
id: self.id,
|
|
rollout_path: self.rollout_path.clone(),
|
|
created_at,
|
|
updated_at,
|
|
source,
|
|
thread_source: self.thread_source,
|
|
agent_nickname: self.agent_nickname.clone(),
|
|
agent_role: self.agent_role.clone(),
|
|
agent_path: self
|
|
.agent_path
|
|
.clone()
|
|
.or_else(|| self.source.get_agent_path().map(Into::into)),
|
|
model_provider: self
|
|
.model_provider
|
|
.clone()
|
|
.unwrap_or_else(|| default_provider.to_string()),
|
|
model: None,
|
|
reasoning_effort: None,
|
|
cwd: self.cwd.clone(),
|
|
cli_version: self.cli_version.clone().unwrap_or_default(),
|
|
title: String::new(),
|
|
preview: None,
|
|
sandbox_policy,
|
|
approval_mode,
|
|
tokens_used: 0,
|
|
first_user_message: None,
|
|
archived_at: self.archived_at.map(canonicalize_datetime),
|
|
git_sha: self.git_sha.clone(),
|
|
git_branch: self.git_branch.clone(),
|
|
git_origin_url: self.git_origin_url.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ThreadMetadata {
|
|
/// Preserve existing non-null Git fields when rollout-derived metadata is reconciled.
|
|
pub fn prefer_existing_git_info(&mut self, existing: &Self) {
|
|
if existing.git_sha.is_some() {
|
|
self.git_sha = existing.git_sha.clone();
|
|
}
|
|
if existing.git_branch.is_some() {
|
|
self.git_branch = existing.git_branch.clone();
|
|
}
|
|
if existing.git_origin_url.is_some() {
|
|
self.git_origin_url = existing.git_origin_url.clone();
|
|
}
|
|
}
|
|
|
|
/// Return the list of field names that differ between `self` and `other`.
|
|
pub fn diff_fields(&self, other: &Self) -> Vec<&'static str> {
|
|
let mut diffs = Vec::new();
|
|
if self.id != other.id {
|
|
diffs.push("id");
|
|
}
|
|
if self.rollout_path != other.rollout_path {
|
|
diffs.push("rollout_path");
|
|
}
|
|
if self.created_at != other.created_at {
|
|
diffs.push("created_at");
|
|
}
|
|
if self.updated_at != other.updated_at {
|
|
diffs.push("updated_at");
|
|
}
|
|
if self.source != other.source {
|
|
diffs.push("source");
|
|
}
|
|
if self.agent_nickname != other.agent_nickname {
|
|
diffs.push("agent_nickname");
|
|
}
|
|
if self.agent_role != other.agent_role {
|
|
diffs.push("agent_role");
|
|
}
|
|
if self.agent_path != other.agent_path {
|
|
diffs.push("agent_path");
|
|
}
|
|
if self.model_provider != other.model_provider {
|
|
diffs.push("model_provider");
|
|
}
|
|
if self.model != other.model {
|
|
diffs.push("model");
|
|
}
|
|
if self.reasoning_effort != other.reasoning_effort {
|
|
diffs.push("reasoning_effort");
|
|
}
|
|
if self.cwd != other.cwd {
|
|
diffs.push("cwd");
|
|
}
|
|
if self.cli_version != other.cli_version {
|
|
diffs.push("cli_version");
|
|
}
|
|
if self.title != other.title {
|
|
diffs.push("title");
|
|
}
|
|
if self.preview != other.preview {
|
|
diffs.push("preview");
|
|
}
|
|
if self.sandbox_policy != other.sandbox_policy {
|
|
diffs.push("sandbox_policy");
|
|
}
|
|
if self.approval_mode != other.approval_mode {
|
|
diffs.push("approval_mode");
|
|
}
|
|
if self.tokens_used != other.tokens_used {
|
|
diffs.push("tokens_used");
|
|
}
|
|
if self.first_user_message != other.first_user_message {
|
|
diffs.push("first_user_message");
|
|
}
|
|
if self.archived_at != other.archived_at {
|
|
diffs.push("archived_at");
|
|
}
|
|
if self.git_sha != other.git_sha {
|
|
diffs.push("git_sha");
|
|
}
|
|
if self.git_branch != other.git_branch {
|
|
diffs.push("git_branch");
|
|
}
|
|
if self.git_origin_url != other.git_origin_url {
|
|
diffs.push("git_origin_url");
|
|
}
|
|
diffs
|
|
}
|
|
}
|
|
|
|
fn canonicalize_datetime(dt: DateTime<Utc>) -> DateTime<Utc> {
|
|
epoch_millis_to_datetime(datetime_to_epoch_millis(dt)).unwrap_or(dt)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct ThreadRow {
|
|
id: String,
|
|
rollout_path: String,
|
|
created_at: i64,
|
|
updated_at: i64,
|
|
source: String,
|
|
thread_source: Option<String>,
|
|
agent_nickname: Option<String>,
|
|
agent_role: Option<String>,
|
|
agent_path: Option<String>,
|
|
model_provider: String,
|
|
model: Option<String>,
|
|
reasoning_effort: Option<String>,
|
|
cwd: String,
|
|
cli_version: String,
|
|
title: String,
|
|
preview: String,
|
|
sandbox_policy: String,
|
|
approval_mode: String,
|
|
tokens_used: i64,
|
|
first_user_message: String,
|
|
archived_at: Option<i64>,
|
|
git_sha: Option<String>,
|
|
git_branch: Option<String>,
|
|
git_origin_url: Option<String>,
|
|
}
|
|
|
|
impl ThreadRow {
|
|
pub(crate) fn try_from_row(row: &SqliteRow) -> Result<Self> {
|
|
Ok(Self {
|
|
id: row.try_get("id")?,
|
|
rollout_path: row.try_get("rollout_path")?,
|
|
created_at: row.try_get("created_at")?,
|
|
updated_at: row.try_get("updated_at")?,
|
|
source: row.try_get("source")?,
|
|
thread_source: row.try_get("thread_source")?,
|
|
agent_nickname: row.try_get("agent_nickname")?,
|
|
agent_role: row.try_get("agent_role")?,
|
|
agent_path: row.try_get("agent_path")?,
|
|
model_provider: row.try_get("model_provider")?,
|
|
model: row.try_get("model")?,
|
|
reasoning_effort: row.try_get("reasoning_effort")?,
|
|
cwd: row.try_get("cwd")?,
|
|
cli_version: row.try_get("cli_version")?,
|
|
title: row.try_get("title")?,
|
|
preview: row.try_get("preview")?,
|
|
sandbox_policy: row.try_get("sandbox_policy")?,
|
|
approval_mode: row.try_get("approval_mode")?,
|
|
tokens_used: row.try_get("tokens_used")?,
|
|
first_user_message: row.try_get("first_user_message")?,
|
|
archived_at: row.try_get("archived_at")?,
|
|
git_sha: row.try_get("git_sha")?,
|
|
git_branch: row.try_get("git_branch")?,
|
|
git_origin_url: row.try_get("git_origin_url")?,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl TryFrom<ThreadRow> for ThreadMetadata {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(row: ThreadRow) -> std::result::Result<Self, Self::Error> {
|
|
let ThreadRow {
|
|
id,
|
|
rollout_path,
|
|
created_at,
|
|
updated_at,
|
|
source,
|
|
thread_source,
|
|
agent_nickname,
|
|
agent_role,
|
|
agent_path,
|
|
model_provider,
|
|
model,
|
|
reasoning_effort,
|
|
cwd,
|
|
cli_version,
|
|
title,
|
|
preview,
|
|
sandbox_policy,
|
|
approval_mode,
|
|
tokens_used,
|
|
first_user_message,
|
|
archived_at,
|
|
git_sha,
|
|
git_branch,
|
|
git_origin_url,
|
|
} = row;
|
|
let thread_source = thread_source
|
|
.map(|thread_source| thread_source.parse())
|
|
.transpose()
|
|
.map_err(anyhow::Error::msg)?;
|
|
Ok(Self {
|
|
id: ThreadId::try_from(id)?,
|
|
rollout_path: PathBuf::from(rollout_path),
|
|
created_at: epoch_millis_to_datetime(created_at)?,
|
|
updated_at: epoch_millis_to_datetime(updated_at)?,
|
|
source,
|
|
thread_source,
|
|
agent_nickname,
|
|
agent_role,
|
|
agent_path,
|
|
model_provider,
|
|
model,
|
|
reasoning_effort: reasoning_effort
|
|
.and_then(|value| value.parse::<ReasoningEffort>().ok()),
|
|
cwd: PathBuf::from(cwd),
|
|
cli_version,
|
|
title,
|
|
preview: (!preview.is_empty()).then_some(preview),
|
|
sandbox_policy,
|
|
approval_mode,
|
|
tokens_used,
|
|
first_user_message: (!first_user_message.is_empty()).then_some(first_user_message),
|
|
archived_at: archived_at.map(epoch_seconds_to_datetime).transpose()?,
|
|
git_sha,
|
|
git_branch,
|
|
git_origin_url,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub(crate) fn anchor_from_item(item: &ThreadMetadata, sort_key: SortKey) -> Option<Anchor> {
|
|
let ts = match sort_key {
|
|
SortKey::CreatedAt => item.created_at,
|
|
SortKey::UpdatedAt => item.updated_at,
|
|
};
|
|
Some(Anchor { ts })
|
|
}
|
|
|
|
pub(crate) fn datetime_to_epoch_millis(dt: DateTime<Utc>) -> i64 {
|
|
dt.timestamp_millis()
|
|
}
|
|
|
|
pub(crate) fn datetime_to_epoch_seconds(dt: DateTime<Utc>) -> i64 {
|
|
dt.timestamp()
|
|
}
|
|
|
|
pub(crate) fn epoch_millis_to_datetime(value: i64) -> Result<DateTime<Utc>> {
|
|
// Values older than 2020 if interpreted as milliseconds are legacy second-precision rows.
|
|
// Convert them in memory so old state DBs keep ordering correctly after new writes use ms.
|
|
const MIN_EPOCH_MILLIS: i64 = 1_577_836_800_000;
|
|
let millis = if value < MIN_EPOCH_MILLIS {
|
|
value.saturating_mul(1000)
|
|
} else {
|
|
value
|
|
};
|
|
DateTime::<Utc>::from_timestamp_millis(millis)
|
|
.ok_or_else(|| anyhow::anyhow!("invalid unix timestamp millis: {value}"))
|
|
}
|
|
|
|
pub(crate) fn epoch_seconds_to_datetime(value: i64) -> Result<DateTime<Utc>> {
|
|
DateTime::<Utc>::from_timestamp(value, 0)
|
|
.ok_or_else(|| anyhow::anyhow!("invalid unix timestamp seconds: {value}"))
|
|
}
|
|
|
|
/// Statistics about a backfill operation.
|
|
#[derive(Debug, Clone)]
|
|
pub struct BackfillStats {
|
|
/// The number of rollout files scanned.
|
|
pub scanned: usize,
|
|
/// The number of rows upserted successfully.
|
|
pub upserted: usize,
|
|
/// The number of rows that failed to upsert.
|
|
pub failed: usize,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::ThreadMetadata;
|
|
use super::ThreadRow;
|
|
use chrono::DateTime;
|
|
use chrono::Utc;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::PathBuf;
|
|
|
|
fn thread_row(reasoning_effort: Option<&str>) -> ThreadRow {
|
|
ThreadRow {
|
|
id: "00000000-0000-0000-0000-000000000123".to_string(),
|
|
rollout_path: "/tmp/rollout-123.jsonl".to_string(),
|
|
created_at: 1_700_000_000,
|
|
updated_at: 1_700_000_100,
|
|
source: "cli".to_string(),
|
|
thread_source: None,
|
|
agent_nickname: None,
|
|
agent_role: None,
|
|
agent_path: None,
|
|
model_provider: "openai".to_string(),
|
|
model: Some("gpt-5".to_string()),
|
|
reasoning_effort: reasoning_effort.map(str::to_string),
|
|
cwd: "/tmp/workspace".to_string(),
|
|
cli_version: "0.0.0".to_string(),
|
|
title: String::new(),
|
|
preview: String::new(),
|
|
sandbox_policy: "read-only".to_string(),
|
|
approval_mode: "on-request".to_string(),
|
|
tokens_used: 1,
|
|
first_user_message: String::new(),
|
|
archived_at: None,
|
|
git_sha: None,
|
|
git_branch: None,
|
|
git_origin_url: None,
|
|
}
|
|
}
|
|
|
|
fn expected_thread_metadata(reasoning_effort: Option<ReasoningEffort>) -> ThreadMetadata {
|
|
ThreadMetadata {
|
|
id: ThreadId::from_string("00000000-0000-0000-0000-000000000123")
|
|
.expect("valid thread id"),
|
|
rollout_path: PathBuf::from("/tmp/rollout-123.jsonl"),
|
|
created_at: DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("timestamp"),
|
|
updated_at: DateTime::<Utc>::from_timestamp(1_700_000_100, 0).expect("timestamp"),
|
|
source: "cli".to_string(),
|
|
thread_source: None,
|
|
agent_nickname: None,
|
|
agent_role: None,
|
|
agent_path: None,
|
|
model_provider: "openai".to_string(),
|
|
model: Some("gpt-5".to_string()),
|
|
reasoning_effort,
|
|
cwd: PathBuf::from("/tmp/workspace"),
|
|
cli_version: "0.0.0".to_string(),
|
|
title: String::new(),
|
|
preview: None,
|
|
sandbox_policy: "read-only".to_string(),
|
|
approval_mode: "on-request".to_string(),
|
|
tokens_used: 1,
|
|
first_user_message: None,
|
|
archived_at: None,
|
|
git_sha: None,
|
|
git_branch: None,
|
|
git_origin_url: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn thread_row_parses_reasoning_effort() {
|
|
let metadata = ThreadMetadata::try_from(thread_row(Some("high")))
|
|
.expect("thread metadata should parse");
|
|
|
|
assert_eq!(
|
|
metadata,
|
|
expected_thread_metadata(Some(ReasoningEffort::High))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn thread_row_ignores_unknown_reasoning_effort_values() {
|
|
let metadata = ThreadMetadata::try_from(thread_row(Some("future")))
|
|
.expect("thread metadata should parse");
|
|
|
|
assert_eq!(
|
|
metadata,
|
|
expected_thread_metadata(/*reasoning_effort*/ None)
|
|
);
|
|
}
|
|
}
|