From 5478e6198c9f4bdf5f74dd0a1c04bffc087c7bf3 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Thu, 21 May 2026 10:22:04 -0700 Subject: [PATCH] fix(ways): pin list context lookup to session id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ways list` was misreporting the context window when the shell cwd was outside the active session's project. The cwd-walking project detection could land on ~/.claude (global config) when no closer .claude marker exists, point the transcript lookup at a non-existent project dir, and fall back to the 200K heuristic — even on opus-4 1M-window sessions. Resolve the transcript by session id directly (scan ~/.claude/projects/*/.jsonl), since the session id is already known by the caller and is globally unique. The cwd-based path remains for the project-scoped `ways context` entrypoint. --- tools/ways-cli/src/cmd/context.rs | 57 ++++++++++++++++++++++++++----- tools/ways-cli/src/cmd/list.rs | 8 +++-- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/tools/ways-cli/src/cmd/context.rs b/tools/ways-cli/src/cmd/context.rs index 6fcc839a..88946f50 100644 --- a/tools/ways-cli/src/cmd/context.rs +++ b/tools/ways-cli/src/cmd/context.rs @@ -20,18 +20,41 @@ pub struct ContextInfo { } /// Get context info for the current session. Used by `ways context` and `ways list`. +/// +/// When `session_id` is provided, the transcript is located by scanning +/// `~/.claude/projects/*/.jsonl` — this is robust against +/// cwd/project mismatches (e.g. a session rooted in `~/.claude` while the +/// shell cwd is elsewhere). Falls back to `project_dir` + newest-transcript +/// lookup when no session id is given. pub fn get_context(project_dir: Option<&str>) -> Result { - let project = project_dir - .map(|s| s.to_string()) - .or_else(|| std::env::var("CLAUDE_PROJECT_DIR").ok()) - .or_else(detect_project_dir) - .unwrap_or_else(|| ".".to_string()); + get_context_inner(project_dir, None) +} + +/// Like `get_context`, but pinned to a known session id. Locates the +/// transcript by session id across all project dirs rather than guessing +/// the project from cwd. +pub fn get_context_for_session(session_id: &str) -> Result { + get_context_inner(None, Some(session_id)) +} - let project_slug = project.replace(['/', '.'], "-"); - let conv_dir = home_dir().join(format!(".claude/projects/{project_slug}")); +fn get_context_inner(project_dir: Option<&str>, session_id: Option<&str>) -> Result { + let transcript = if let Some(sid) = session_id { + find_transcript_by_session(sid).ok_or_else(|| { + anyhow::anyhow!("No transcript found for session: {sid}") + })? + } else { + let project = project_dir + .map(|s| s.to_string()) + .or_else(|| std::env::var("CLAUDE_PROJECT_DIR").ok()) + .or_else(detect_project_dir) + .unwrap_or_else(|| ".".to_string()); + + let project_slug = project.replace(['/', '.'], "-"); + let conv_dir = home_dir().join(format!(".claude/projects/{project_slug}")); - let transcript = find_newest_transcript(&conv_dir) - .ok_or_else(|| anyhow::anyhow!("No active transcript found for project: {project}"))?; + find_newest_transcript(&conv_dir) + .ok_or_else(|| anyhow::anyhow!("No active transcript found for project: {project}"))? + }; let session = transcript .file_stem() @@ -222,6 +245,22 @@ fn read_token_usage(content: &str) -> (u64, String) { (estimated, "bytes".to_string()) } +/// Find a transcript by session id, searching every project dir under +/// `~/.claude/projects/`. Session ids are globally unique, so we don't +/// need to know which project the session is rooted in. +fn find_transcript_by_session(session_id: &str) -> Option { + let projects_root = home_dir().join(".claude/projects"); + let filename = format!("{session_id}.jsonl"); + for entry in std::fs::read_dir(&projects_root).ok()? { + let entry = entry.ok()?; + let candidate = entry.path().join(&filename); + if candidate.is_file() { + return Some(candidate); + } + } + None +} + fn find_newest_transcript(dir: &Path) -> Option { if !dir.is_dir() { return None; diff --git a/tools/ways-cli/src/cmd/list.rs b/tools/ways-cli/src/cmd/list.rs index 3777793a..63568472 100644 --- a/tools/ways-cli/src/cmd/list.rs +++ b/tools/ways-cli/src/cmd/list.rs @@ -56,8 +56,12 @@ pub fn run(session: Option<&str>, sort: &str, json_out: bool) -> Result<()> { let current_epoch = session::get_epoch(&session_id); - // Use accurate context data from transcript when available - let (current_tokens_k, context_window_k, context_window) = match context::get_context(None) { + // Use accurate context data from transcript when available. + // Pinning to session_id keeps detection correct when cwd is outside + // the session's project (e.g. a session rooted in ~/.claude while the + // shell has cd'd elsewhere) — otherwise the cwd-walk can land on + // ~/.claude (global config) and miss the real transcript. + let (current_tokens_k, context_window_k, context_window) = match context::get_context_for_session(&session_id) { Ok(ctx) => (ctx.tokens_used / 1000, ctx.tokens_total / 1000, ctx.tokens_total), Err(_) => { // Fallback to session markers