Summary
On Windows, every /codex:transfer fails with:
Codex reported that the Claude import completed, but did not record an imported thread.
Check the Codex app-server logs for the underlying import error.
…even though the import actually succeeds: a real thread is created and a matching record (with a valid imported_thread_id) is appended to ~/.codex/external_agent_session_imports.json. The error is a false negative in the post-import lookup, so users never get their codex resume <id> command and silently accumulate one orphaned thread per attempt.
Environment
- Plugin:
codex@openai-codex v1.0.6
- Codex CLI:
codex-cli 0.144.2
- Node: v24.15.0
- OS: Windows 11 (x64)
Root cause
Both are in scripts/lib/codex.mjs → importedThreadIdForSource(sourcePath), which looks up the just-written ledger record using two equality checks that both fail on Windows:
const canonicalSource = fs.realpathSync(sourcePath);
const contentSha256 = sourceContentSha256(canonicalSource);
const match = records
.filter(
(record) =>
record?.source_path === canonicalSource && // (1)
record?.content_sha256 === contentSha256 && // (2)
typeof record?.imported_thread_id === "string"
)
.at(-1);
(1) Extended-length path prefix mismatch. The ledger (written by the Codex binary) stores source_path with the Windows \\?\ extended-length prefix, but fs.realpathSync() in Node returns the path without it, so record.source_path === canonicalSource is never true.
ledger source_path : \\?\C:\Users\<user>\.claude\projects\<proj>\<id>.jsonl
realpathSync() : C:\Users\<user>\.claude\projects\<proj>\<id>.jsonl
(2) Live-file content-hash race. The Claude transcript .jsonl is a live, append-only file. content_sha256 is recorded at import time, but importedThreadIdForSource re-hashes the file after the import — and by then the transcript has grown (the transfer command's own turn is written to it), so the hash differs.
Either problem alone is enough to make the lookup return null. Verified against a real ledger: after normalizing the path, the record matches even though the current hash no longer equals the stored one.
Reproduction
- On Windows, run
/codex:transfer from an active Claude Code session.
- Observe the "did not record an imported thread" error.
- Inspect
~/.codex/external_agent_session_imports.json — a new record with a valid imported_thread_id is present; source_path starts with \\?\.
codex resume <that id> works fine.
Suggested fix
Normalize paths before comparison (strip \\?\, and on Windows unify separators + case), and treat the content hash as a preference rather than a hard requirement — fall back to the most recent import for that source:
const records = Array.isArray(ledger?.records) ? ledger.records : [];
// Strip the Windows "\\?\" extended-length prefix (present in the ledger but
// not in realpathSync output) and, on Windows, unify separators + case.
const normalizePath = (p) => {
if (typeof p !== "string") return "";
let s = p.replace(/^\\\\\?\\/, "").replace(/^\/\/\?\//, "");
if (process.platform === "win32") s = s.replace(/\//g, "\\").toLowerCase();
return s;
};
const target = normalizePath(canonicalSource);
const bySource = records.filter(
(record) =>
normalizePath(record?.source_path) === target &&
typeof record?.imported_thread_id === "string"
);
// The transcript is live/append-only, so its hash can change between import and
// this lookup. Prefer an exact content-hash match, else the newest import.
const exact = bySource.filter((record) => record.content_sha256 === contentSha256).at(-1);
const match = exact ?? bySource.at(-1);
return match?.imported_thread_id ?? null;
With this change, /codex:transfer on Windows returns the correct thread and prints the expected codex resume <id> line.
Summary
On Windows, every
/codex:transferfails with:…even though the import actually succeeds: a real thread is created and a matching record (with a valid
imported_thread_id) is appended to~/.codex/external_agent_session_imports.json. The error is a false negative in the post-import lookup, so users never get theircodex resume <id>command and silently accumulate one orphaned thread per attempt.Environment
codex@openai-codexv1.0.6codex-cli 0.144.2Root cause
Both are in
scripts/lib/codex.mjs→importedThreadIdForSource(sourcePath), which looks up the just-written ledger record using two equality checks that both fail on Windows:(1) Extended-length path prefix mismatch. The ledger (written by the Codex binary) stores
source_pathwith the Windows\\?\extended-length prefix, butfs.realpathSync()in Node returns the path without it, sorecord.source_path === canonicalSourceis never true.(2) Live-file content-hash race. The Claude transcript
.jsonlis a live, append-only file.content_sha256is recorded at import time, butimportedThreadIdForSourcere-hashes the file after the import — and by then the transcript has grown (the transfer command's own turn is written to it), so the hash differs.Either problem alone is enough to make the lookup return
null. Verified against a real ledger: after normalizing the path, the record matches even though the current hash no longer equals the stored one.Reproduction
/codex:transferfrom an active Claude Code session.~/.codex/external_agent_session_imports.json— a new record with a validimported_thread_idis present;source_pathstarts with\\?\.codex resume <that id>works fine.Suggested fix
Normalize paths before comparison (strip
\\?\, and on Windows unify separators + case), and treat the content hash as a preference rather than a hard requirement — fall back to the most recent import for that source:With this change,
/codex:transferon Windows returns the correct thread and prints the expectedcodex resume <id>line.