use std::path::PathBuf;
use anyhow::{bail, Result};
use serde_json::json;
use crate::cli::{OutputFormat, WhoamiArgs};
use crate::live::channel;
use crate::path;
const SESSION_ID_ENV: &str = "CLAUDE_CODE_SESSION_ID";
const SESSION_ID_ENV_ALIAS: &str = "CODEX_COMPANION_SESSION_ID";
pub const AMBIGUOUS_GUIDANCE: &str = "cannot identify the calling session: \
CLAUDE_CODE_SESSION_ID is not set (old Claude Code build, or running outside Claude \
Code). Do NOT trust most-recent-mtime — many sessions may be live at once. Pass an \
explicit `@<uuid>` target: your id is the basename of your own transcript jsonl, \
or grep a unique recent line you wrote to disambiguate.";
#[must_use]
pub fn detect_session_id() -> Option<String> {
if let Ok(v) = std::env::var(SESSION_ID_ENV) {
if !v.trim().is_empty() {
return Some(v.trim().to_string());
}
}
if let Ok(v) = std::env::var(SESSION_ID_ENV_ALIAS) {
if !v.trim().is_empty() {
return Some(v.trim().to_string());
}
}
None
}
#[derive(Debug, Clone)]
pub struct WhoAmI {
pub session_id: String,
pub path: Option<PathBuf>,
}
pub fn run_whoami(args: &WhoamiArgs) -> Result<()> {
if args.peers {
return channel::run_peers(args.format);
}
if let Some(to) = args.to.as_deref() {
return channel::run_reach_to(to, args.format);
}
match args.self_target.as_deref() {
None | Some("@main") => run_whoami_env(args),
Some(t) if t.starts_with("@trap:") => {
run_whoami_trap(t.strip_prefix("@trap:").unwrap_or(""), args)
}
Some(t) if is_lane_target(t) => run_whoami_lane(t, args),
Some(other) => bail!(
"whoami accepts no target except `@trap:<marker>` (which SUBAGENT am I?), `@main` \
(the calling top-level session — the default), or a LANE id `@<agent-id>` / \
`@<Name>@<Team>` (answer for that lane). Got `{other}`. A session uuid is not a \
whoami question: to inspect a DIFFERENT session, use `csift list @<uuid>` / \
`csift agents @<uuid>`."
),
}
}
fn is_lane_target(token: &str) -> bool {
let Some(id) = token.strip_prefix('@') else {
return false;
};
path::is_subagent_id(id) || path::is_teammate_routing_id(id)
}
fn run_whoami_env(args: &WhoamiArgs) -> Result<()> {
let Some(session_id) = detect_session_id() else {
channel::external_answer(args.format)?;
bail!("{AMBIGUOUS_GUIDANCE}");
};
let path = locate_transcript(&session_id);
eprintln!(
"csift: note: resolved via env ($CLAUDE_CODE_SESSION_ID), which names the TOP-LEVEL \
session in every lane - from a subagent this is the PARENT's id, not yours. Lane \
fields are null under env-only resolution; a first-try `whoami @trap:<marker>` hit \
proves your true lane."
);
let me = WhoAmI { session_id, path };
match args.format {
OutputFormat::Text => render_text(&me),
OutputFormat::Json => render_json_head(&me)?,
}
let counts = match &me.path {
Some(p) => Some(sections_for(p, false, "env", args.format)?),
None => None,
};
finish_json(args.format, 1, counts)
}
fn sections_for(
path: &std::path::Path,
exact: bool,
via: &'static str,
format: OutputFormat,
) -> Result<(usize, usize)> {
channel::emit_lane_sections(
&channel::LaneRef {
path: path.to_path_buf(),
exact,
via,
},
format,
)
}
fn run_whoami_lane(token: &str, args: &WhoamiArgs) -> Result<()> {
let path = channel::resolve_lane(token)?;
if matches!(args.format, OutputFormat::Json) {
println!("{}", crate::text::envelope_header("whoami", json!({})));
}
let counts = sections_for(&path, true, "target", args.format)?;
finish_json(args.format, 0, Some(counts))
}
fn finish_json(
format: OutputFormat,
identities: usize,
counts: Option<(usize, usize)>,
) -> Result<()> {
if !matches!(format, OutputFormat::Json) {
return Ok(());
}
let (live, others) = counts.unwrap_or((0, 0));
println!(
"{}",
crate::text::envelope_summary(json!({
"identities": identities,
"live_child_lanes": live,
"other_live_lanes": others,
}))
);
Ok(())
}
fn run_whoami_trap(marker: &str, args: &WhoamiArgs) -> Result<()> {
let chain = path::resolve_trap_who(marker)?;
match args.format {
OutputFormat::Text => {
for (i, n) in chain.iter().enumerate() {
let role = if n.is_subagent { "subagent" } else { "session" };
let annot = match (i, n.is_subagent, n.depth) {
(0, true, Some(d)) => format!(" <- you (subagent, depth {d})"),
(0, true, None) => " <- you (subagent)".to_string(),
(0, false, _) => " <- you (top-level session, not a subagent)".to_string(),
(_, true, Some(d)) => format!(" ^ parent subagent (depth {d})"),
(_, true, None) => " ^ parent subagent".to_string(),
(_, false, _) => " ^ top-level root".to_string(),
};
println!("{role:8} {}{annot}", n.session_id);
}
match chain.first().and_then(|n| n.path.as_ref()) {
Some(p) => println!("path {}", p.display()),
None => println!("path <transcript not found under projects root>"),
}
}
OutputFormat::Json => {
println!("{}", crate::text::envelope_header("whoami", json!({})));
for n in &chain {
let obj = json!({
"kind": "identity",
"session_id": n.session_id,
"is_subagent": n.is_subagent,
"parent_session_id": n.parent_session_id,
"depth": n.depth,
"path": n.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
});
println!("{}", serde_json::to_string(&obj)?);
}
}
}
let counts = match chain.first().and_then(|n| n.path.as_ref()) {
Some(p) => Some(sections_for(p, true, "trap", args.format)?),
None => None,
};
finish_json(args.format, chain.len(), counts)
}
fn locate_transcript(session_id: &str) -> Option<PathBuf> {
let root = path::projects_root().ok()?;
let filename = format!("{session_id}.jsonl");
if let Ok(cwd) = std::env::current_dir() {
let dir = root.join(path::encode_cwd(&cwd));
let candidate = dir.join(&filename);
if candidate.is_file() {
return Some(candidate);
}
}
let dirs = path::all_project_dirs().ok()?;
for pd in dirs {
let candidate = pd.dir.join(&filename);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
fn render_text(me: &WhoAmI) {
println!("session {}", me.session_id);
println!("lane unknown from env alone (a subagent sees its parent's id here)");
match &me.path {
Some(p) => println!("path {}", p.display()),
None => println!("path <not found under projects root for the current cwd>"),
}
}
fn render_json_head(me: &WhoAmI) -> Result<()> {
println!("{}", crate::text::envelope_header("whoami", json!({})));
let obj = json!({
"kind": "identity",
"session_id": me.session_id,
"is_subagent": serde_json::Value::Null,
"parent_session_id": serde_json::Value::Null,
"depth": serde_json::Value::Null,
"path": me.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
});
println!("{}", serde_json::to_string(&obj)?);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn guidance_mentions_session_target_and_no_mtime() {
assert!(AMBIGUOUS_GUIDANCE.contains("@<uuid>"));
assert!(AMBIGUOUS_GUIDANCE.contains("mtime"));
assert!(AMBIGUOUS_GUIDANCE.contains("CLAUDE_CODE_SESSION_ID"));
}
#[test]
fn exact_env_name_is_canonical_not_a_loose_regex() {
assert_eq!(SESSION_ID_ENV, "CLAUDE_CODE_SESSION_ID");
assert_ne!(SESSION_ID_ENV, "SECURITYSESSIONID");
}
#[test]
fn detect_trims_and_blank_is_none() {
assert!(" ".trim().is_empty());
}
}