Skip to main content

agentmux_srv/backend/
continuity_relocate.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Same-identity relocation
5//! (SPEC_RESUME_GATE_AND_SAME_IDENTITY_CONTINUATION_2026_09_25.md §4.3): the
6//! file half. When the agent's chain head lives under another login of the
7//! same Anthropic identity, one session file is copied into the spawn's
8//! config dir so `--resume <head> --fork-session` can read it. The fork
9//! writes a new session and never touches the copy or the original (verified
10//! against Claude Code 2.1.280, spec §2.4), so the copy exists only until the
11//! fork reports its own id.
12//!
13//! Every copy carries a marker naming the agent that made it, so the next
14//! spawn of that agent can remove one a dead process left behind, and no
15//! other agent's copy is touched. The marker is written before the copy
16//! takes its real name: a copy is never visible to the CLI without one, so
17//! a crash can't leave a lasting second file with the head's id (spec §3 I4).
18//! Each marker's name carries a token of its own, so a relocation settles
19//! only its own copy, never a later one placed at the same path.
20
21use std::path::{Path, PathBuf};
22
23use crate::backend::session_backfill::session_file_path;
24
25const MARKER_SUFFIX: &str = ".agentmux-relocated";
26const TMP_INFIX: &str = ".agentmux-tmp-";
27/// How much of a session's end is read first to find its last record; more
28/// is read when the record is larger.
29const TAIL_CHECK_BYTES: u64 = 256 * 1024;
30
31/// The session file `sid` for `cwd` under `config_dir`, or under the
32/// always-global history dir of the same login when the channel-local one
33/// is gone (a cleaned-up build channel): `channels/<c>/identities/<id>/claude`
34/// has its `projects/` linked to `shared/identities/<id>/claude/projects`.
35pub(crate) fn source_file(config_dir: &str, cwd: &str, sid: &str) -> Option<PathBuf> {
36    let local = session_file_path(config_dir, cwd, sid)?;
37    if local.is_file() {
38        return Some(local);
39    }
40    let global = global_twin(config_dir)?;
41    let path = session_file_path(&global.to_string_lossy(), cwd, sid)?;
42    path.is_file().then_some(path)
43}
44
45/// `<shared>/identities/<id>/<rest…>` for a config dir under some
46/// `…/identities/<id>/<rest…>`, when that differs from `config_dir`.
47fn global_twin(config_dir: &str) -> Option<PathBuf> {
48    let parts: Vec<String> =
49        Path::new(config_dir).components().map(|c| c.as_os_str().to_string_lossy().into_owned()).collect();
50    let at = parts.iter().rposition(|p| p == "identities")?;
51    let rest = parts.get(at + 1..).filter(|r| !r.is_empty())?;
52    let mut twin = crate::registry::resolve_global_shared_root()?.join("identities");
53    for p in rest {
54        twin.push(p);
55    }
56    (twin != Path::new(config_dir)).then_some(twin)
57}
58
59/// Whether `path` is a session the CLI can resume: a non-empty file whose
60/// last record parses (spec H5 — a file cut mid-write is not resumed).
61pub(crate) fn is_resumable_file(path: &Path) -> bool {
62    last_record(path, false).is_ok_and(|(_, record)| parses(&record))
63}
64
65/// Whether session `sid` for `cwd` under `config_dir` can be copied in by
66/// [`relocate`]: it exists ([`source_file`]) and holds a complete record. Its
67/// last record may still be being written, by a parent a fork starts from
68/// mid-turn: [`relocate`] cuts the copy back to its last complete one.
69pub(crate) fn can_relocate(config_dir: &str, cwd: &str, sid: &str) -> bool {
70    source_file(config_dir, cwd, sid)
71        .and_then(|p| last_record(&p, true).ok())
72        .is_some_and(|(_, record)| parses(&record))
73}
74
75/// `path`'s last record and the offset where it ends. With `complete_only`
76/// that is the last newline-ended record, so a record still being written
77/// is passed over; a file with no newline at all is one record either way,
78/// left to the parse to judge. Reads the file's end, widening the window
79/// until it holds the whole record, however large (a big tool result).
80fn last_record(path: &Path, complete_only: bool) -> std::io::Result<(u64, Vec<u8>)> {
81    use std::io::{Read, Seek, SeekFrom};
82    let mut f = std::fs::File::open(path)?;
83    let len = f.metadata()?.len();
84    let mut window = TAIL_CHECK_BYTES;
85    loop {
86        let start = len.saturating_sub(window);
87        f.seek(SeekFrom::Start(start))?;
88        let mut tail = Vec::new();
89        f.read_to_end(&mut tail)?;
90        let newline_before = |at: usize| tail[..at].iter().rposition(|b| *b == b'\n');
91        let end = match (complete_only, newline_before(tail.len())) {
92            (false, _) => Some(tail.len()),
93            (true, Some(at)) => Some(at + 1),
94            (true, None) => (start == 0).then_some(tail.len()),
95        };
96        if let Some(end) = end {
97            let body = tail[..end].iter().rposition(|b| !b.is_ascii_whitespace()).map_or(0, |at| at + 1);
98            match newline_before(body) {
99                Some(at) => return Ok((start + end as u64, tail[at + 1..body].to_vec())),
100                None if start == 0 => return Ok((end as u64, tail[..body].to_vec())),
101                None => {}
102            }
103        }
104        window = window.saturating_mul(4);
105    }
106}
107
108/// Whether `record` is one JSON value (an empty one isn't: an empty file
109/// isn't resumable).
110fn parses(record: &[u8]) -> bool {
111    serde_json::from_slice::<serde_json::Value>(record).is_ok()
112}
113
114/// A copy [`relocate`] placed, with the marker that makes it this
115/// relocation's. Each relocation's marker has its own name, so holding one
116/// is ownership: once a sweep has taken it (and a later relocation placed a
117/// new copy at the same path, under a new marker), settling through the old
118/// handle finds its marker gone and touches nothing (codex P1 on #3907).
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub(crate) struct Relocated {
121    pub copy: PathBuf,
122    marker: PathBuf,
123}
124
125/// Copy `src` in as session `sid` for `cwd` under `dest_config_dir`, marked
126/// as `owner_uid`'s. Refuses when the destination already exists: an
127/// existing file with the head's id is never overwritten (spec §4.3 (5)).
128pub(crate) fn relocate(src: &Path, dest_config_dir: &str, cwd: &str, sid: &str, owner_uid: &str) -> Result<Relocated, String> {
129    let dest = session_file_path(dest_config_dir, cwd, sid).ok_or("no destination path")?;
130    if dest.exists() {
131        return Err(format!("{} already exists", dest.display()));
132    }
133    let dir = dest.parent().ok_or("destination has no parent")?;
134    std::fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
135    // Both names carry this relocation's token: once placed, the temp name is
136    // a hard link to the copy, so a concurrent relocation reusing it would
137    // write straight into the winner's copy (codex P1 on #3924).
138    let token = uuid::Uuid::new_v4().simple().to_string();
139    let tmp = dir.join(format!("{sid}.jsonl{TMP_INFIX}{owner_uid}.{token}"));
140    let bytes = match std::fs::copy(src, &tmp).and_then(|_| trim_to_complete_records(&tmp)) {
141        Ok(b) => b,
142        Err(e) => {
143            let _ = std::fs::remove_file(&tmp);
144            return Err(format!("copy {}: {e}", src.display()));
145        }
146    };
147    // The source may be live: a fork may start while its parent writes a
148    // turn. What was copied is cut back to its last complete record above, a
149    // stable snapshot the fork branches from; it is placed only if the CLI
150    // can resume it.
151    if !is_resumable_file(&tmp) {
152        let _ = std::fs::remove_file(&tmp);
153        return Err(format!("{} has no complete record to fork from", src.display()));
154    }
155    let marker = marker_for(&dest, &token);
156    let record = serde_json::json!({ "owner_uid": owner_uid, "source": src.to_string_lossy(), "bytes": bytes });
157    let placed = std::fs::write(&marker, record.to_string())
158        .map_err(|e| format!("marker {}: {e}", marker.display()))
159        .and_then(|()| place(&tmp, &dest).map_err(|e| format!("place {}: {e}", dest.display())));
160    let _ = std::fs::remove_file(&tmp);
161    if let Err(e) = placed {
162        let _ = std::fs::remove_file(&marker);
163        return Err(e);
164    }
165    Ok(Relocated { copy: dest, marker })
166}
167
168/// Cut `path` back to its last complete (newline-ended) record: a copy of a
169/// session file another process is appending to may end mid-record. Returns
170/// the kept length. A tail with no newline at all is left as it is; the
171/// resumability check that follows judges it.
172fn trim_to_complete_records(path: &Path) -> std::io::Result<u64> {
173    let (keep, _) = last_record(path, true)?;
174    let f = std::fs::OpenOptions::new().write(true).open(path)?;
175    if keep < f.metadata()?.len() {
176        f.set_len(keep)?;
177    }
178    Ok(keep)
179}
180
181/// Give the finished copy its real name, only if nothing has it: two spawns
182/// relocating the same head at once both pass the `exists` check, and a
183/// rename would let the second replace the first's copy under both markers
184/// (codex P1 on #3924). A hard link fails on an existing name, atomically.
185fn place(tmp: &Path, dest: &Path) -> std::io::Result<()> {
186    std::fs::hard_link(tmp, dest)
187}
188
189/// Remove a copy [`relocate`] placed, and its marker, if it is still this
190/// relocation's. The marker goes first: only a relocation that still held
191/// it removes the copy. Returns whether it did.
192pub(crate) fn remove(r: &Relocated) -> bool {
193    if !unmark(r) {
194        return false;
195    }
196    remove_file(&r.copy);
197    true
198}
199
200/// Drop a copy's marker only, keeping the copy: the CLI resumed it in place,
201/// so it is a live session now and no sweep may remove it. Returns whether
202/// the copy was still this relocation's.
203pub(crate) fn unmark(r: &Relocated) -> bool {
204    match std::fs::remove_file(&r.marker) {
205        Ok(()) => true,
206        Err(e) => {
207            if e.kind() != std::io::ErrorKind::NotFound {
208                tracing::warn!(target: "continuity", path = %r.marker.display(), error = %e, "relocated copy: unmark failed");
209            }
210            false
211        }
212    }
213}
214
215fn remove_file(p: &Path) {
216    if let Err(e) = std::fs::remove_file(p) {
217        if e.kind() != std::io::ErrorKind::NotFound {
218            tracing::warn!(target: "continuity", path = %p.display(), error = %e, "relocated copy: remove failed");
219        }
220    }
221}
222
223/// Remove every copy `owner_uid` left in `cwd`'s project dir under
224/// `config_dir` (a process that died before its fork reported an id), and
225/// any half-made one. Returns how many copies went. Other agents' copies
226/// and every ordinary session are left alone.
227pub(crate) fn sweep(config_dir: &str, cwd: &str, owner_uid: &str) -> usize {
228    let Some(dir) = session_file_path(config_dir, cwd, "x").and_then(|p| p.parent().map(Path::to_path_buf)) else {
229        return 0;
230    };
231    let Ok(entries) = std::fs::read_dir(&dir) else { return 0 };
232    // `<sid>.jsonl.agentmux-tmp-<uid>.<token>`, or without the token from
233    // before tokens.
234    let tmp_tag = format!("{TMP_INFIX}{owner_uid}");
235    let mut removed = 0;
236    for entry in entries.flatten() {
237        let name = entry.file_name().to_string_lossy().into_owned();
238        if name.ends_with(&tmp_tag) || name.contains(&format!("{tmp_tag}.")) {
239            let _ = std::fs::remove_file(entry.path());
240        } else if let Some(copy_name) = name.strip_suffix(MARKER_SUFFIX).map(copy_name_of) {
241            let owner = std::fs::read_to_string(entry.path())
242                .ok()
243                .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
244                .and_then(|v| v.get("owner_uid").and_then(|o| o.as_str()).map(str::to_string));
245            if owner.as_deref() == Some(owner_uid) {
246                remove(&Relocated { copy: dir.join(copy_name), marker: entry.path() });
247                removed += 1;
248            }
249        }
250    }
251    removed
252}
253
254/// `<copy>.<token>.agentmux-relocated`: the token makes each relocation's
255/// marker its own.
256fn marker_for(copy: &Path, token: &str) -> PathBuf {
257    let mut name = copy.file_name().map(|n| n.to_os_string()).unwrap_or_default();
258    name.push(format!(".{token}{MARKER_SUFFIX}"));
259    copy.with_file_name(name)
260}
261
262/// The copy a marker (its name without the suffix) is for. A marker from
263/// before tokens is `<sid>.jsonl.agentmux-relocated`.
264fn copy_name_of(stem: &str) -> &str {
265    if stem.ends_with(".jsonl") {
266        stem
267    } else {
268        stem.rsplit_once('.').map_or(stem, |(copy, _token)| copy)
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    const CWD: &str = "/agents/agenta";
277
278    fn cfg() -> tempfile::TempDir {
279        tempfile::tempdir().unwrap()
280    }
281
282    fn write_session(config_dir: &Path, sid: &str, body: &str) -> PathBuf {
283        let p = session_file_path(&config_dir.to_string_lossy(), CWD, sid).unwrap();
284        std::fs::create_dir_all(p.parent().unwrap()).unwrap();
285        std::fs::write(&p, body).unwrap();
286        p
287    }
288
289    fn s(p: &Path) -> String {
290        p.to_string_lossy().into_owned()
291    }
292
293    #[test]
294    fn a_complete_session_is_resumable_and_a_cut_or_empty_one_is_not() {
295        let d = cfg();
296        let good = write_session(d.path(), "good", "{\"a\":1}\n{\"b\":2}\n");
297        let cut = write_session(d.path(), "cut", "{\"a\":1}\n{\"b\":");
298        let empty = write_session(d.path(), "empty", "");
299        assert!(is_resumable_file(&good));
300        assert!(!is_resumable_file(&cut), "a file cut mid-record");
301        assert!(!is_resumable_file(&empty));
302        assert!(!is_resumable_file(&d.path().join("missing.jsonl")));
303    }
304
305    /// A last record larger than the first read of the file's end (a big
306    /// tool result) is still found whole (#4748).
307    #[test]
308    fn a_record_larger_than_the_tail_window_is_read_whole() {
309        let (from, to) = (cfg(), cfg());
310        let big = format!("{{\"result\":\"{}\"}}
311", "x".repeat(TAIL_CHECK_BYTES as usize * 2));
312        let src = write_session(from.path(), "big", &format!("{{\"turn\":1}}
313{big}{{\"tur"));
314        assert!(!is_resumable_file(&src));
315        assert!(can_relocate(&s(from.path()), CWD, "big"));
316        let r = relocate(&src, &s(to.path()), CWD, "big", "uid-1").unwrap();
317        assert_eq!(std::fs::read_to_string(&r.copy).unwrap(), format!("{{\"turn\":1}}
318{big}"));
319        assert!(is_resumable_file(&r.copy));
320    }
321
322    /// A parent mid-turn can still be forked from: the gate asks whether a
323    /// copy would hold a complete record, not whether the live file ends on
324    /// one (#4748).
325    #[test]
326    fn a_source_mid_record_can_be_relocated_but_not_resumed_in_place() {
327        let d = cfg();
328        let live = write_session(d.path(), "live", "{\"turn\":1}
329{\"tur");
330        write_session(d.path(), "partial", "{\"tur");
331        assert!(!is_resumable_file(&live));
332        assert!(can_relocate(&s(d.path()), CWD, "live"));
333        assert!(!can_relocate(&s(d.path()), CWD, "partial"), "no complete record");
334        assert!(!can_relocate(&s(d.path()), CWD, "missing"));
335    }
336
337    /// A source still being written may end mid-record: the copy stops at the
338    /// last complete one, and a source with none is refused (#4748).
339    #[test]
340    fn a_copy_of_a_live_session_ends_on_its_last_complete_record() {
341        let (from, to) = (cfg(), cfg());
342        let src = write_session(from.path(), "live", "{\"turn\":1}
343{\"turn\":2}
344{\"tur");
345        let r = relocate(&src, &s(to.path()), CWD, "live", "uid-1").unwrap();
346        assert_eq!(std::fs::read_to_string(&r.copy).unwrap(), "{\"turn\":1}
347{\"turn\":2}
348");
349        assert!(std::fs::read_to_string(&src).unwrap().ends_with("{\"tur"), "the source is untouched");
350
351        let (from, to) = (cfg(), cfg());
352        let src = write_session(from.path(), "partial", "{\"tur");
353        assert!(relocate(&src, &s(to.path()), CWD, "partial", "uid-1").is_err());
354        let dest = session_file_path(&s(to.path()), CWD, "partial").unwrap();
355        assert!(!dest.exists(), "nothing placed");
356        assert_eq!(std::fs::read_dir(dest.parent().unwrap()).map(|d| d.count()).unwrap_or(0), 0, "no temp copy left");
357    }
358
359    #[test]
360    fn relocation_copies_marks_and_never_touches_the_original() {
361        let (from, to) = (cfg(), cfg());
362        let src = write_session(from.path(), "head", "{\"turn\":1}\n");
363        let r = relocate(&src, &s(to.path()), CWD, "head", "uid-1").unwrap();
364        assert_eq!(std::fs::read_to_string(&r.copy).unwrap(), "{\"turn\":1}\n");
365        assert_eq!(r.copy, session_file_path(&s(to.path()), CWD, "head").unwrap());
366        assert!(r.marker.is_file(), "a copy is never unmarked");
367        assert_eq!(std::fs::read_to_string(&src).unwrap(), "{\"turn\":1}\n");
368        let leftovers: Vec<_> = std::fs::read_dir(r.copy.parent().unwrap())
369            .unwrap()
370            .flatten()
371            .filter(|e| e.file_name().to_string_lossy().contains(TMP_INFIX))
372            .collect();
373        assert!(leftovers.is_empty(), "no half-made copy remains");
374    }
375
376    #[test]
377    fn an_existing_session_is_never_overwritten() {
378        let (from, to) = (cfg(), cfg());
379        let src = write_session(from.path(), "head", "{\"from\":\"elsewhere\"}\n");
380        let existing = write_session(to.path(), "head", "{\"mine\":true}\n");
381        assert!(relocate(&src, &s(to.path()), CWD, "head", "uid-1").is_err());
382        assert_eq!(std::fs::read_to_string(&existing).unwrap(), "{\"mine\":true}\n");
383        let markers = std::fs::read_dir(existing.parent().unwrap())
384            .unwrap()
385            .flatten()
386            .filter(|e| e.file_name().to_string_lossy().ends_with(MARKER_SUFFIX))
387            .count();
388        assert_eq!(markers, 0);
389    }
390
391    #[test]
392    fn remove_takes_the_copy_and_its_marker() {
393        let (from, to) = (cfg(), cfg());
394        let src = write_session(from.path(), "head", "{}\n");
395        let r = relocate(&src, &s(to.path()), CWD, "head", "uid-1").unwrap();
396        assert!(remove(&r));
397        assert!(!r.copy.exists() && !r.marker.exists());
398        assert!(!remove(&r), "gone already: quiet, and not this relocation's any more");
399    }
400
401    /// A replacement spawn swept this relocation's copy and placed its own at
402    /// the same path: the old handle settles nothing (codex P1 on #3907).
403    #[test]
404    fn a_stale_handle_never_touches_a_later_relocation_at_the_same_path() {
405        let (from, to) = (cfg(), cfg());
406        let src = write_session(from.path(), "head", "{}\n");
407        let old = relocate(&src, &s(to.path()), CWD, "head", "uid-1").unwrap();
408        assert_eq!(sweep(&s(to.path()), CWD, "uid-1"), 1);
409        let new = relocate(&src, &s(to.path()), CWD, "head", "uid-1").unwrap();
410        assert_eq!(old.copy, new.copy);
411
412        assert!(!unmark(&old));
413        assert!(!remove(&old));
414        assert!(new.copy.exists() && new.marker.exists(), "the replacement's copy is still its own");
415        assert!(unmark(&new));
416    }
417
418    /// The loser of two concurrent relocations of one head fails to place its
419    /// copy instead of replacing the winner's.
420    #[test]
421    fn placing_never_replaces_a_copy_already_there() {
422        let d = cfg();
423        let dest = write_session(d.path(), "head", "{\"winner\":true}\n");
424        let tmp = dest.with_file_name("head.jsonl.agentmux-tmp-uid-2");
425        std::fs::write(&tmp, "{\"loser\":true}\n").unwrap();
426        assert_eq!(place(&tmp, &dest).unwrap_err().kind(), std::io::ErrorKind::AlreadyExists);
427        assert_eq!(std::fs::read_to_string(&dest).unwrap(), "{\"winner\":true}\n");
428    }
429
430    /// Each relocation copies through its own temp name, so none can write
431    /// into another's placed copy through a shared one (codex P1 on #3924).
432    #[test]
433    fn a_relocation_leaves_no_writable_alias_of_its_copy() {
434        let (from, to) = (cfg(), cfg());
435        let src = write_session(from.path(), "head", "{\"turn\":1}\n");
436        let r = relocate(&src, &s(to.path()), CWD, "head", "uid-1").unwrap();
437        let names: Vec<String> = std::fs::read_dir(r.copy.parent().unwrap())
438            .unwrap()
439            .flatten()
440            .map(|e| e.file_name().to_string_lossy().into_owned())
441            .collect();
442        assert!(!names.iter().any(|n| n.contains(TMP_INFIX)), "no temp name left: {names:?}");
443    }
444
445    #[test]
446    fn half_made_copies_are_swept_with_or_without_a_token_but_only_this_agents() {
447        let to = cfg();
448        let dir = write_session(to.path(), "ordinary", "{}\n").parent().unwrap().to_path_buf();
449        let tokened = dir.join(format!("head.jsonl{TMP_INFIX}uid-1.abc123"));
450        let legacy = dir.join(format!("other.jsonl{TMP_INFIX}uid-1"));
451        let longer_uid = dir.join(format!("head.jsonl{TMP_INFIX}uid-10.abc123"));
452        for p in [&tokened, &legacy, &longer_uid] {
453            std::fs::write(p, "{}").unwrap();
454        }
455        sweep(&s(to.path()), CWD, "uid-1");
456        assert!(!tokened.exists() && !legacy.exists());
457        assert!(longer_uid.exists(), "uid-10's temp file is not uid-1's");
458    }
459
460    #[test]
461    fn a_marker_from_before_tokens_is_still_swept() {
462        let to = cfg();
463        let copy = write_session(to.path(), "head", "{}\n");
464        let legacy = copy.with_file_name(format!("head.jsonl{MARKER_SUFFIX}"));
465        std::fs::write(&legacy, r#"{"owner_uid":"uid-1"}"#).unwrap();
466        assert_eq!(sweep(&s(to.path()), CWD, "uid-1"), 1);
467        assert!(!copy.exists() && !legacy.exists());
468    }
469
470    #[test]
471    fn an_unmarked_copy_is_a_live_session_no_sweep_touches() {
472        let (from, to) = (cfg(), cfg());
473        let src = write_session(from.path(), "head", "{}\n");
474        let r = relocate(&src, &s(to.path()), CWD, "head", "uid-1").unwrap();
475        assert!(unmark(&r));
476        assert_eq!(sweep(&s(to.path()), CWD, "uid-1"), 0);
477        assert!(r.copy.exists());
478    }
479
480    #[test]
481    fn a_sweep_removes_only_this_agents_copies() {
482        let (from, to) = (cfg(), cfg());
483        let src = write_session(from.path(), "head", "{}\n");
484        let mine = relocate(&src, &s(to.path()), CWD, "head", "uid-1").unwrap();
485        let src2 = write_session(from.path(), "other", "{}\n");
486        let theirs = relocate(&src2, &s(to.path()), CWD, "other", "uid-2").unwrap();
487        let ordinary = write_session(to.path(), "ordinary", "{}\n");
488        let half_made = mine.copy.parent().unwrap().join(format!("x.jsonl{TMP_INFIX}uid-1"));
489        std::fs::write(&half_made, "{}").unwrap();
490
491        assert_eq!(sweep(&s(to.path()), CWD, "uid-1"), 1);
492        assert!(!mine.copy.exists() && !mine.marker.exists());
493        assert!(!half_made.exists());
494        assert!(theirs.copy.exists() && theirs.marker.exists(), "another agent's copy stays");
495        assert!(ordinary.exists(), "an ordinary session stays");
496        assert_eq!(sweep(&s(to.path()), CWD, "uid-1"), 0);
497        assert_eq!(sweep("/no/such/dir", CWD, "uid-1"), 0);
498    }
499
500    #[test]
501    fn the_source_is_found_under_the_login_s_global_history_when_the_channel_dir_is_gone() {
502        // The crate-wide lock every `AGENTMUX_HOME_OVERRIDE` consumer takes.
503        let _g = crate::test_support::ISOLATED_AUTH_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
504        let home = cfg();
505        std::env::set_var("AGENTMUX_HOME_OVERRIDE", home.path());
506        let gone_channel_dir = home.path().join("channels").join("old-build").join("identities").join("acct-1").join("claude");
507        let global = home.path().join("shared").join("identities").join("acct-1").join("claude");
508        let path = write_session(&global, "head", "{}\n");
509        let found = source_file(&s(&gone_channel_dir), CWD, "head");
510        let missing = source_file(&s(&gone_channel_dir), CWD, "missing");
511        std::env::remove_var("AGENTMUX_HOME_OVERRIDE");
512        assert_eq!(found, Some(path));
513        assert_eq!(missing, None);
514    }
515}