Skip to content

/codex:transfer always reports "did not record an imported thread" on Windows (thread is actually created) #513

Description

@SUN-bef-dev

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.mjsimportedThreadIdForSource(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

  1. On Windows, run /codex:transfer from an active Claude Code session.
  2. Observe the "did not record an imported thread" error.
  3. Inspect ~/.codex/external_agent_session_imports.json — a new record with a valid imported_thread_id is present; source_path starts with \\?\.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions