use std::env;
use std::fs::{self, File};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
use serde_json::Value;
use super::string_key;
use crate::telemetry::host::{self as host_telemetry, HostContextSnapshot};
pub(crate) fn current() -> Result<Option<HostContextSnapshot>> {
let thread_id = match env::var("CODEX_THREAD_ID") {
Ok(value) if !value.trim().is_empty() => value,
_ => return Ok(None),
};
let codex_home = codex_home()?;
let session_path = match find_session_log(&codex_home.join("sessions"), &thread_id)? {
Some(path) => path,
None => return Ok(None),
};
let observed_at_epoch_s = match host_telemetry::file_mtime_epoch_s(&session_path)? {
Some(epoch_s) => epoch_s,
None => return Ok(None),
};
latest_token_count(&session_path, observed_at_epoch_s)
}
fn codex_home() -> Result<PathBuf> {
if let Some(home) = env::var_os("CODEX_HOME") {
return Ok(PathBuf::from(home));
}
match env::var_os("HOME") {
Some(home) => Ok(PathBuf::from(home).join(".codex")),
None => anyhow::bail!("HOME environment variable is not set"),
}
}
fn find_session_log(root: &Path, thread_id: &str) -> Result<Option<PathBuf>> {
if !root.is_dir() {
return Ok(None);
}
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in
fs::read_dir(&dir).with_context(|| format!("failed to read {}", dir.display()))?
{
let entry =
entry.with_context(|| format!("failed to read entry in {}", dir.display()))?;
let path = entry.path();
if path.is_dir() {
stack.push(path);
continue;
}
let matches = path
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.contains(thread_id) && name.ends_with(".jsonl"))
.unwrap_or(false);
if matches {
return Ok(Some(path));
}
}
}
Ok(None)
}
fn latest_token_count(
path: &Path,
observed_at_epoch_s: u64,
) -> Result<Option<HostContextSnapshot>> {
let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
let reader = BufReader::new(file);
let mut latest = None;
let mut latest_model = host_telemetry::env_string(&["CCD_CODEX_MODEL", "CODEX_MODEL"])
.or_else(|| host_telemetry::env_string(&["CCD_HOST_MODEL"]));
for line in reader.lines() {
let line = line.with_context(|| format!("failed to read {}", path.display()))?;
let value: Value = match serde_json::from_str(&line) {
Ok(value) => value,
Err(_) => continue,
};
if let Some(model_name) = string_key(
&value,
&[
"model",
"model_name",
"modelName",
"model_slug",
"modelSlug",
],
) {
latest_model = Some(model_name);
}
let entry: SessionEntry = match serde_json::from_value(value) {
Ok(entry) => entry,
Err(_) => continue,
};
if entry.entry_type != "event_msg" {
continue;
}
let payload = match entry.payload {
Some(payload) => payload,
None => continue,
};
if payload.payload_type != "token_count" {
continue;
}
let native_window = match payload.info.model_context_window {
0 => None,
value => Some(value),
};
let context_window = native_window.or_else(host_telemetry::general_context_window);
let total_tokens = payload
.info
.last_token_usage
.as_ref()
.map(|usage| usage.total_tokens)
.unwrap_or(payload.info.total_token_usage.total_tokens);
latest = Some(HostContextSnapshot {
host: "codex",
observed_at_epoch_s,
model_name: latest_model.clone(),
context_used_pct: host_telemetry::compute_pct(total_tokens, context_window),
total_tokens: Some(total_tokens),
model_context_window: context_window,
compacted: None,
cost_usage: host_telemetry::HostCostUsage {
blended_total_tokens: Some(payload.info.total_token_usage.total_tokens),
..host_telemetry::HostCostUsage::default()
},
});
}
Ok(latest)
}
#[derive(Deserialize)]
struct SessionEntry {
#[serde(rename = "type")]
entry_type: String,
payload: Option<TokenCountPayload>,
}
#[derive(Deserialize)]
struct TokenCountPayload {
#[serde(rename = "type")]
payload_type: String,
info: TokenCountInfo,
}
#[derive(Deserialize)]
struct TokenCountInfo {
total_token_usage: TotalTokenUsage,
last_token_usage: Option<TotalTokenUsage>,
model_context_window: u64,
}
#[derive(Deserialize)]
struct TotalTokenUsage {
total_tokens: u64,
}