Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,35 @@ cache the same queries run ~4.7× faster (e.g. `length` 966 ms →
drivers so the zero-file rule lives once.
- The emscripten (playground) build keeps the sequential engine —
no threads there.
## `toolpath-codex`: the caller supplies the home directory — 2026-08-13

- **`toolpath-codex`** (0.7.0): breaking. `PathResolver::new(home)`
takes the home directory as a required argument. The crate reads no
environment variable; it keeps the layout knowledge (`<home>/.codex`)
and the caller owns "what is home". `CodexConvo::new(home)` and
`ConvoIO::new(home)` take the same argument.

Removed: the `Default` impls on `PathResolver`, `ConvoIO`, and
`CodexConvo`; `PathResolver::with_home`; the `NoHomeDirectory` and
`CodexDirectoryNotFound` error variants. `with_codex_dir` stays as
the full override.

The home directory is always present, so `home_dir()`, `codex_dir()`,
`sessions_root()`, `history_file()`, `log_file()`, and
`ConvoIO::codex_dir_path()` return a path instead of a `Result`.

Strict rollout parsing is a parameter. `CodexConvo::with_strict(bool)`
and `ConvoIO::with_strict(bool)` set it,
`RolloutReader::read_session_with(path, strict)` takes it directly,
and `RolloutReader::read_session(path)` stays lenient. The crate reads
no environment variable for it.
- **`path-cli`** (unreleased): `providers::codex_resolver` returns
`Option<PathResolver>`. `None` means the configuration carries no home
directory, so Codex is out of reach: the harness bundle omits it, and
a command that targets Codex reports "cannot determine the home
directory". `Config` reads `$CODEX_ROLLOUT_STRICT` and passes the flag
to every `CodexConvo` it builds, so the variable keeps its behavior
for CLI users.

## `toolpath-gemini`: the caller supplies the home directory — 2026-08-13

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ toolpath-convo = { version = "0.11.1", path = "crates/toolpath-convo" }
toolpath-git = { version = "0.6.0", path = "crates/toolpath-git" }
toolpath-claude = { version = "0.12.2", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.7.0", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.6.1", path = "crates/toolpath-codex" }
toolpath-codex = { version = "0.7.0", path = "crates/toolpath-codex" }
toolpath-copilot = { version = "0.1.0", path = "crates/toolpath-copilot" }
toolpath-opencode = { version = "0.5.0", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.2.0", path = "crates/toolpath-cursor" }
Expand Down
19 changes: 5 additions & 14 deletions crates/path-cli/src/cmd_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1221,10 +1221,8 @@ fn build_codex_session(config: &Config, input: &str, cwd: &str) -> Result<toolpa
#[cfg(not(target_os = "emscripten"))]
fn write_into_codex_project(session: &toolpath_codex::Session, config: &Config) -> Result<()> {
let session_ts = codex_session_timestamp(session)?;
let resolver = providers::codex_resolver(config);
let sessions_root = resolver
.sessions_root()
.map_err(|e| anyhow::anyhow!("Cannot resolve Codex sessions dir: {}", e))?;
let resolver = providers::require_codex_resolver(config)?;
let sessions_root = resolver.sessions_root();

// sessions/YYYY/MM/DD/
let date_dir = sessions_root
Expand All @@ -1240,9 +1238,7 @@ fn write_into_codex_project(session: &toolpath_codex::Session, config: &Config)

// `codex resume` reads from state_5.sqlite, not the filesystem;
// without a thread row the rollout file is invisible.
let codex_dir = resolver
.codex_dir()
.map_err(|e| anyhow::anyhow!("Cannot resolve ~/.codex dir: {}", e))?;
let codex_dir = resolver.codex_dir();
let registration = register_codex_thread(&codex_dir, session, &out_path, &session_ts);

eprintln!(
Expand Down Expand Up @@ -2879,13 +2875,8 @@ mod tests {
)
.expect("export codex --project");

let resolver = PathResolver::new().with_home(&fake_home);
let dated_dir = resolver
.sessions_root()
.unwrap()
.join("2026")
.join("05")
.join("15");
let resolver = PathResolver::new(&fake_home);
let dated_dir = resolver.sessions_root().join("2026").join("05").join("15");
assert!(
dated_dir.exists(),
"expected dated sessions dir at {}",
Expand Down
4 changes: 3 additions & 1 deletion crates/path-cli/src/cmd_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,9 @@ fn pick_gemini_global(
}

fn derive_codex(session: Option<String>, all: bool, config: &Config) -> Result<Vec<DerivedDoc>> {
let manager = toolpath_codex::CodexConvo::with_resolver(providers::codex_resolver(config));
let manager =
toolpath_codex::CodexConvo::with_resolver(providers::require_codex_resolver(config)?)
.with_strict(providers::codex_strict(config));

let session_ids: Vec<String> = match (session, all) {
(Some(s), _) => vec![s],
Expand Down
4 changes: 3 additions & 1 deletion crates/path-cli/src/cmd_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,9 @@ fn emit_gemini_tsv(m: &toolpath_gemini::ConversationMetadata) {
// ── Codex ───────────────────────────────────────────────────────────────────

fn run_codex(fmt: ListFormat, config: &Config) -> Result<()> {
let manager = toolpath_codex::CodexConvo::with_resolver(providers::codex_resolver(config));
let manager =
toolpath_codex::CodexConvo::with_resolver(providers::require_codex_resolver(config)?)
.with_strict(providers::codex_strict(config));
let sessions = manager
.list_sessions()
.map_err(|e| anyhow::anyhow!("{}", e))?;
Expand Down
12 changes: 5 additions & 7 deletions crates/path-cli/src/cmd_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,12 +699,10 @@ fn harness_status_codex(bundle: &HarnessBundle, home: Option<&std::path::Path>)
let Some(mgr) = &bundle.codex else {
return HarnessStatus::unresolved();
};
match mgr.resolver().sessions_root() {
Ok(p) => HarnessStatus {
path: crate::config::home_relative(&p, home),
exists: p.exists(),
},
Err(_) => HarnessStatus::unresolved(),
let p = mgr.resolver().sessions_root();
HarnessStatus {
path: crate::config::home_relative(&p, home),
exists: p.exists(),
}
}

Expand Down Expand Up @@ -1114,7 +1112,7 @@ mod tests {
fn codex_only_bundle(home: &Path) -> HarnessBundle {
let codex_dir = home.join(".codex");
std::fs::create_dir_all(&codex_dir).unwrap();
let resolver = toolpath_codex::PathResolver::new().with_codex_dir(&codex_dir);
let resolver = toolpath_codex::PathResolver::new(home).with_codex_dir(&codex_dir);
HarnessBundle {
codex: Some(toolpath_codex::CodexConvo::with_resolver(resolver)),
..Default::default()
Expand Down
6 changes: 4 additions & 2 deletions crates/path-cli/src/cmd_show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,10 @@ fn derive_one(source: ShowSource, config: &Config) -> Result<toolpath::v1::Path>
session,
project: _,
} => {
let manager =
toolpath_codex::CodexConvo::with_resolver(providers::codex_resolver(config));
let manager = toolpath_codex::CodexConvo::with_resolver(
providers::require_codex_resolver(config)?,
)
.with_strict(providers::codex_strict(config));
let s = manager
.read_session(&session)
.map_err(|e| anyhow::anyhow!("{}", e))?;
Expand Down
7 changes: 7 additions & 0 deletions crates/path-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub(crate) const DOCUMENTS_DIR_NAME: &str = "documents";
/// field name matches its variable.
const CONFIG_ENV_VARS: &[&str] = &[
"APPDATA",
"CODEX_ROLLOUT_STRICT",
"COPILOT_HOME",
"HOME",
PATHBASE_URL_ENV,
Expand All @@ -68,6 +69,10 @@ const CONFIG_ENV_VARS: &[&str] = &[
pub struct Config {
/// `$APPDATA`: Windows harness data root.
pub(crate) appdata: Option<PathBuf>,
/// `$CODEX_ROLLOUT_STRICT`: the Codex reader errors on an
/// unparseable rollout line. Presence is the signal; the value is
/// not read.
pub(crate) codex_rollout_strict: Option<String>,
/// `$COPILOT_HOME`: Copilot CLI session root override.
pub(crate) copilot_home: Option<PathBuf>,
/// `$HOME`: config-root fallback and the harness resolvers' root.
Expand Down Expand Up @@ -179,6 +184,7 @@ mod tests {
figment::Jail::expect_with(|jail| {
jail.set_env(CONFIG_DIR_ENV, "/tmp/cfg-root");
jail.set_env("HOME", "/home/jailed");
jail.set_env("CODEX_ROLLOUT_STRICT", "1");
jail.set_env("XDG_DATA_HOME", "/home/jailed/.local/share");
jail.set_env("COPILOT_HOME", "/home/jailed/.copilot");
jail.set_env("APPDATA", "/home/jailed/appdata");
Expand All @@ -190,6 +196,7 @@ mod tests {
config,
Config {
appdata: Some(PathBuf::from("/home/jailed/appdata")),
codex_rollout_strict: Some("1".to_string()),
copilot_home: Some(PathBuf::from("/home/jailed/.copilot")),
home: Some(PathBuf::from("/home/jailed")),
pathbase_url: Some("https://pathbase.test".to_string()),
Expand Down
3 changes: 2 additions & 1 deletion crates/path-cli/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ pub(crate) fn derive_gemini_session_with(
/// Derive a single Codex session given an explicit session id.
pub(crate) fn derive_codex_session(config: &Config, session: &str) -> Result<DerivedDoc> {
derive_codex_session_with(
&toolpath_codex::CodexConvo::with_resolver(providers::codex_resolver(config)),
&toolpath_codex::CodexConvo::with_resolver(providers::require_codex_resolver(config)?)
.with_strict(providers::codex_strict(config)),
session,
)
}
Expand Down
2 changes: 0 additions & 2 deletions crates/path-cli/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,6 @@ pub(crate) fn is_not_found_pi(err: &toolpath_pi::PiError) -> bool {
pub(crate) fn is_not_found_codex(err: &toolpath_codex::ConvoError) -> bool {
use toolpath_codex::ConvoError;
matches!(err, ConvoError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
|| matches!(err, ConvoError::NoHomeDirectory)
|| matches!(err, ConvoError::CodexDirectoryNotFound(_))
}

pub(crate) fn is_not_found_copilot(err: &toolpath_copilot::ConvoError) -> bool {
Expand Down
46 changes: 35 additions & 11 deletions crates/path-cli/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,19 @@ pub(crate) fn require_gemini_resolver(config: &Config) -> Result<toolpath_gemini
gemini_resolver(config).ok_or_else(|| missing_home("Gemini"))
}

pub(crate) fn codex_resolver(config: &Config) -> toolpath_codex::PathResolver {
let mut resolver = toolpath_codex::PathResolver::new();
if let Some(home) = config.home_dir() {
resolver = resolver.with_home(home);
}
resolver
pub(crate) fn codex_resolver(config: &Config) -> Option<toolpath_codex::PathResolver> {
config.home_dir().map(toolpath_codex::PathResolver::new)
}

/// [`codex_resolver`] for a command that targets Codex.
pub(crate) fn require_codex_resolver(config: &Config) -> Result<toolpath_codex::PathResolver> {
codex_resolver(config).ok_or_else(|| missing_home("Codex"))
}

/// The Codex reader's strict flag. `$CODEX_ROLLOUT_STRICT` is strict
/// when set, whatever its value.
pub(crate) fn codex_strict(config: &Config) -> bool {
config.codex_rollout_strict.is_some()
}

pub(crate) fn copilot_resolver(config: &Config) -> toolpath_copilot::PathResolver {
Expand Down Expand Up @@ -108,9 +115,9 @@ pub(crate) fn harness_bundle(config: &Config) -> HarnessBundle {
claude_resolver(config),
)),
gemini: gemini_resolver(config).map(toolpath_gemini::GeminiConvo::with_resolver),
codex: Some(toolpath_codex::CodexConvo::with_resolver(codex_resolver(
config,
))),
codex: codex_resolver(config).map(|r| {
toolpath_codex::CodexConvo::with_resolver(r).with_strict(codex_strict(config))
}),
copilot: Some(toolpath_copilot::CopilotConvo::with_resolver(
copilot_resolver(config),
)),
Expand Down Expand Up @@ -165,13 +172,30 @@ mod tests {

#[test]
fn codex_resolver_roots_at_config_home() {
let resolver = codex_resolver(&config_with_home());
let resolver = codex_resolver(&config_with_home()).unwrap();
assert_eq!(
resolver.sessions_root().unwrap(),
resolver.sessions_root(),
PathBuf::from("/home/jailed/.codex/sessions")
);
}

#[test]
fn codex_resolver_is_none_without_a_home() {
assert!(codex_resolver(&Config::default()).is_none());
let err = require_codex_resolver(&Config::default()).unwrap_err();
assert!(err.to_string().contains("home directory"));
}

#[test]
fn codex_strict_follows_presence_of_the_variable() {
assert!(!codex_strict(&Config::default()));
let config = Config {
codex_rollout_strict: Some(String::new()),
..Config::default()
};
assert!(codex_strict(&config));
}

#[test]
fn copilot_resolver_injects_copilot_dir() {
let config = Config {
Expand Down
2 changes: 1 addition & 1 deletion crates/path-cli/src/sync/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ mod tests {
format!("{meta}\n{user}\n"),
)
.unwrap();
let resolver = toolpath_codex::PathResolver::new().with_codex_dir(&codex_dir);
let resolver = toolpath_codex::PathResolver::new(home).with_codex_dir(&codex_dir);
HarnessBundle {
codex: Some(toolpath_codex::CodexConvo::with_resolver(resolver)),
..Default::default()
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-codex/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toolpath-codex"
version = "0.6.1"
version = "0.7.0"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-codex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ documents so every Codex-assisted change has a traceable origin.
```rust,no_run
use toolpath_codex::{CodexConvo, derive::{DeriveConfig, derive_path}};

let manager = CodexConvo::new();
let manager = CodexConvo::new("/Users/alex");
let session_id = "019dabc6-8fef-7681-a054-b5bb75fcb97d";
let convo = manager.read_session(session_id)?;
let path = derive_path(&convo, &DeriveConfig::default());
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-codex/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ mod tests {
fs::create_dir_all(&day).unwrap();
let name = "rollout-2026-04-20T10-00-00-019dabc6-8fef-7681-a054-b5bb75fcb97d";
fs::write(day.join(format!("{}.jsonl", name)), body).unwrap();
let resolver = crate::PathResolver::new().with_codex_dir(&codex);
let resolver = crate::PathResolver::new(temp.path()).with_codex_dir(&codex);
(temp, CodexConvo::with_resolver(resolver), name.into())
}

Expand Down
6 changes: 0 additions & 6 deletions crates/toolpath-codex/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ pub enum ConvoError {
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),

#[error("Home directory not found")]
NoHomeDirectory,

#[error("Codex directory not found at path: {0}")]
CodexDirectoryNotFound(PathBuf),

#[error("Session not found: {0}")]
SessionNotFound(String),

Expand Down
Loading
Loading