Merge pull request #2630 from Hmbown/codex/v0.8.52-home-cost-fixes
fix(release): tighten 0.8.52 home and cost accounting
This commit is contained in:
+11
-3
@@ -47,14 +47,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
`POST /messages` requests on stale legacy SSE sessions now trigger the same
|
||||
reconnect-and-retry path as closed SSE streams, removing a release-gate flake
|
||||
and matching the intended recovery behavior (#2597).
|
||||
- **Cache-hit cost accounting uses one telemetry source.** Mixed DeepSeek
|
||||
`prompt_cache_hit_tokens` and OpenAI-style `cached_tokens` usage payloads no
|
||||
longer infer cache misses from the wrong hit count, avoiding inflated TUI cost
|
||||
estimates on cached DeepSeek turns (#2567, #2609).
|
||||
- **Cygwin/MSYS2 config paths honor exported `$HOME`.** CodeWhale and legacy
|
||||
DeepSeek config roots now prefer a non-empty `$HOME` before falling back to the
|
||||
platform home resolver, while `CODEWHALE_HOME` remains the strongest explicit
|
||||
override (#2369, #2610).
|
||||
|
||||
### Community
|
||||
|
||||
Thanks to **@xyuai** (#2587), **@IcedOranges** (#2584), **@BH8GCJ** (#2588),
|
||||
**@shenjackyuanjie** (#2618, #2619), **@idling11** (#2606, #2616),
|
||||
**@AresNing** (#2578), **@gordonlu**, **@encyc**, and **@simuusang** (#2603,
|
||||
#2620) for reports, patches, retesting, and release-stabilization signals that
|
||||
shaped this pass.
|
||||
**@AresNing** (#2578), **@caiyilian** (#2567), **@buko** (#2369),
|
||||
**@gordonlu**, **@encyc**, and **@simuusang** (#2603, #2620) for reports,
|
||||
patches, retesting, and release-stabilization signals that shaped this pass.
|
||||
|
||||
## [0.8.51] - 2026-06-02
|
||||
|
||||
|
||||
@@ -2038,7 +2038,7 @@ pub fn codewhale_home() -> Result<PathBuf> {
|
||||
return Ok(PathBuf::from(trimmed));
|
||||
}
|
||||
}
|
||||
let home = dirs::home_dir().context("failed to resolve home directory")?;
|
||||
let home = effective_home_dir().context("failed to resolve home directory")?;
|
||||
Ok(home.join(CODEWHALE_APP_DIR))
|
||||
}
|
||||
|
||||
@@ -2046,10 +2046,17 @@ pub fn codewhale_home() -> Result<PathBuf> {
|
||||
///
|
||||
/// Always returns the legacy path regardless of whether it exists.
|
||||
pub fn legacy_deepseek_home() -> Result<PathBuf> {
|
||||
let home = dirs::home_dir().context("failed to resolve home directory")?;
|
||||
let home = effective_home_dir().context("failed to resolve home directory")?;
|
||||
Ok(home.join(LEGACY_APP_DIR))
|
||||
}
|
||||
|
||||
fn effective_home_dir() -> Option<PathBuf> {
|
||||
std::env::var_os("HOME")
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.or_else(dirs::home_dir)
|
||||
}
|
||||
|
||||
/// Resolve a state subdirectory, preferring the CodeWhale root if
|
||||
/// it already exists, otherwise falling back to the legacy root.
|
||||
///
|
||||
@@ -2507,7 +2514,9 @@ mod tests {
|
||||
|
||||
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3485,6 +3494,73 @@ unix_socket_path = "/tmp/cw-hooks.sock"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_homes_prefer_home_env_before_platform_home_fallback() {
|
||||
let _lock = env_lock();
|
||||
struct HomeEnvGuard {
|
||||
home: Option<OsString>,
|
||||
userprofile: Option<OsString>,
|
||||
codewhale_home: Option<OsString>,
|
||||
}
|
||||
|
||||
impl Drop for HomeEnvGuard {
|
||||
fn drop(&mut self) {
|
||||
// Safety: test-only environment mutation is serialized by env_lock().
|
||||
unsafe {
|
||||
match self.home.take() {
|
||||
Some(value) => env::set_var("HOME", value),
|
||||
None => env::remove_var("HOME"),
|
||||
}
|
||||
match self.userprofile.take() {
|
||||
Some(value) => env::set_var("USERPROFILE", value),
|
||||
None => env::remove_var("USERPROFILE"),
|
||||
}
|
||||
match self.codewhale_home.take() {
|
||||
Some(value) => env::set_var("CODEWHALE_HOME", value),
|
||||
None => env::remove_var("CODEWHALE_HOME"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let home =
|
||||
std::env::temp_dir().join(format!("codewhale-config-home-env-{}", std::process::id()));
|
||||
let userprofile = std::env::temp_dir().join(format!(
|
||||
"codewhale-config-userprofile-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _env = HomeEnvGuard {
|
||||
home: env::var_os("HOME"),
|
||||
userprofile: env::var_os("USERPROFILE"),
|
||||
codewhale_home: env::var_os("CODEWHALE_HOME"),
|
||||
};
|
||||
// Safety: test-only environment mutation is serialized by env_lock().
|
||||
unsafe {
|
||||
env::set_var("HOME", &home);
|
||||
env::set_var("USERPROFILE", &userprofile);
|
||||
env::remove_var("CODEWHALE_HOME");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
codewhale_home().expect("codewhale home"),
|
||||
home.join(CODEWHALE_APP_DIR)
|
||||
);
|
||||
assert_eq!(
|
||||
legacy_deepseek_home().expect("legacy home"),
|
||||
home.join(LEGACY_APP_DIR)
|
||||
);
|
||||
|
||||
let explicit = std::env::temp_dir().join(format!(
|
||||
"codewhale-config-explicit-home-{}",
|
||||
std::process::id()
|
||||
));
|
||||
// Safety: test-only environment mutation is serialized by env_lock().
|
||||
unsafe {
|
||||
env::set_var("CODEWHALE_HOME", &explicit);
|
||||
}
|
||||
assert_eq!(codewhale_home().expect("explicit home"), explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_config_reports_copied_legacy_path() {
|
||||
let _lock = env_lock();
|
||||
@@ -3550,9 +3626,6 @@ unix_socket_path = "/tmp/cw-hooks.sock"
|
||||
"codewhale-config-migration-{}-{unique}",
|
||||
std::process::id()
|
||||
));
|
||||
#[cfg(windows)]
|
||||
let legacy_dir = legacy_deepseek_home().expect("legacy home");
|
||||
#[cfg(not(windows))]
|
||||
let legacy_dir = home.join(LEGACY_APP_DIR);
|
||||
let primary_dir = home.join(CODEWHALE_APP_DIR);
|
||||
let legacy_config = legacy_dir.join(CONFIG_FILE_NAME);
|
||||
|
||||
+11
-3
@@ -47,14 +47,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
`POST /messages` requests on stale legacy SSE sessions now trigger the same
|
||||
reconnect-and-retry path as closed SSE streams, removing a release-gate flake
|
||||
and matching the intended recovery behavior (#2597).
|
||||
- **Cache-hit cost accounting uses one telemetry source.** Mixed DeepSeek
|
||||
`prompt_cache_hit_tokens` and OpenAI-style `cached_tokens` usage payloads no
|
||||
longer infer cache misses from the wrong hit count, avoiding inflated TUI cost
|
||||
estimates on cached DeepSeek turns (#2567, #2609).
|
||||
- **Cygwin/MSYS2 config paths honor exported `$HOME`.** CodeWhale and legacy
|
||||
DeepSeek config roots now prefer a non-empty `$HOME` before falling back to the
|
||||
platform home resolver, while `CODEWHALE_HOME` remains the strongest explicit
|
||||
override (#2369, #2610).
|
||||
|
||||
### Community
|
||||
|
||||
Thanks to **@xyuai** (#2587), **@IcedOranges** (#2584), **@BH8GCJ** (#2588),
|
||||
**@shenjackyuanjie** (#2618, #2619), **@idling11** (#2606, #2616),
|
||||
**@AresNing** (#2578), **@gordonlu**, **@encyc**, and **@simuusang** (#2603,
|
||||
#2620) for reports, patches, retesting, and release-stabilization signals that
|
||||
shaped this pass.
|
||||
**@AresNing** (#2578), **@caiyilian** (#2567), **@buko** (#2369),
|
||||
**@gordonlu**, **@encyc**, and **@simuusang** (#2603, #2620) for reports,
|
||||
patches, retesting, and release-stabilization signals that shaped this pass.
|
||||
|
||||
## [0.8.51] - 2026-06-02
|
||||
|
||||
|
||||
@@ -1275,7 +1275,7 @@ pub(super) fn parse_usage(usage: Option<&Value>) -> Usage {
|
||||
let prompt_cache_miss_tokens = usage
|
||||
.and_then(|u| u.get("prompt_cache_miss_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
.or_else(|| cached_tokens.map(|cached| input_tokens.saturating_sub(cached)))
|
||||
.or_else(|| prompt_cache_hit_tokens.map(|hit| input_tokens.saturating_sub(u64::from(hit))))
|
||||
.map(|v| v as u32);
|
||||
let reasoning_tokens = reasoning_tokens_raw.map(|v| v as u32);
|
||||
|
||||
@@ -3078,6 +3078,22 @@ mod tests {
|
||||
assert_eq!(usage.prompt_cache_miss_tokens, Some(1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_usage_infers_cache_miss_from_selected_hit_source() {
|
||||
let usage = parse_usage(Some(&json!({
|
||||
"prompt_tokens": 4000,
|
||||
"completion_tokens": 20,
|
||||
"prompt_cache_hit_tokens": 3000,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 1000
|
||||
}
|
||||
})));
|
||||
|
||||
assert_eq!(usage.input_tokens, 4000);
|
||||
assert_eq!(usage.prompt_cache_hit_tokens, Some(3000));
|
||||
assert_eq!(usage.prompt_cache_miss_tokens, Some(1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_thinking_mode_counts_reasoning_replay_across_assistant_turns() {
|
||||
// Multi-turn body that mimics two prior tool-calling rounds: each
|
||||
|
||||
@@ -161,6 +161,10 @@ pub fn calculate_turn_cost(model: &str, input_tokens: u32, output_tokens: u32) -
|
||||
}
|
||||
|
||||
/// Calculate cost for a turn in both official currencies.
|
||||
///
|
||||
/// This legacy helper has no cache telemetry, so it prices all input tokens as
|
||||
/// cache misses. Prefer [`calculate_turn_cost_estimate_from_usage`] when the
|
||||
/// provider returned usage details.
|
||||
#[must_use]
|
||||
pub fn calculate_turn_cost_estimate(
|
||||
model: &str,
|
||||
|
||||
@@ -965,7 +965,7 @@ async fn test_running_count_counts_running_agents_until_status_reconciles() {
|
||||
#[tokio::test]
|
||||
async fn cleanup_auto_cancels_stale_running_agent_and_releases_slot() {
|
||||
let mut manager = SubAgentManager::new(PathBuf::from("."), 1)
|
||||
.with_running_heartbeat_timeout(Duration::from_secs(300));
|
||||
.with_running_heartbeat_timeout(Duration::from_millis(1));
|
||||
let (input_tx, _input_rx) = mpsc::unbounded_channel();
|
||||
let mut agent = SubAgent::new(
|
||||
"test_agent_stale".to_string(),
|
||||
@@ -978,12 +978,12 @@ async fn cleanup_auto_cancels_stale_running_agent_and_releases_slot() {
|
||||
input_tx,
|
||||
"boot_test".to_string(),
|
||||
);
|
||||
agent.last_activity_at = instant_from_duration(Duration::from_secs(600));
|
||||
agent.task_handle = Some(tokio::spawn(async {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}));
|
||||
let agent_id = agent.id.clone();
|
||||
manager.agents.insert(agent_id.clone(), agent);
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
|
||||
assert_eq!(
|
||||
manager.running_count(),
|
||||
@@ -1046,7 +1046,7 @@ async fn cleanup_keeps_recent_running_agent() {
|
||||
#[tokio::test]
|
||||
async fn touch_refreshes_stale_running_agent_heartbeat() {
|
||||
let mut manager = SubAgentManager::new(PathBuf::from("."), 1)
|
||||
.with_running_heartbeat_timeout(Duration::from_secs(300));
|
||||
.with_running_heartbeat_timeout(Duration::from_millis(1));
|
||||
let (input_tx, _input_rx) = mpsc::unbounded_channel();
|
||||
let mut agent = SubAgent::new(
|
||||
"test_agent_touched".to_string(),
|
||||
@@ -1059,12 +1059,12 @@ async fn touch_refreshes_stale_running_agent_heartbeat() {
|
||||
input_tx,
|
||||
"boot_test".to_string(),
|
||||
);
|
||||
agent.last_activity_at = instant_from_duration(Duration::from_secs(600));
|
||||
agent.task_handle = Some(tokio::spawn(async {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}));
|
||||
let agent_id = agent.id.clone();
|
||||
manager.agents.insert(agent_id.clone(), agent);
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
|
||||
assert_eq!(manager.running_count(), 0);
|
||||
assert!(manager.touch(&agent_id));
|
||||
|
||||
Reference in New Issue
Block a user