Max Nardit
← 記事
14 mindev

Claude Code のメモリを Codex 0.165 にインポートする:最初のインポートより前に memories を有効にしないと、プロファイルは v1 のまま

ノートのコピー自体は簡単な部分です。Claude Code のファイルはすべてそのまま残り、Codex がセッション開始時に読み込むのは、サイズ上限が厳格に決まったサマリーの中にモデルが書いた、そのファイルへの道筋です。デフォルトでオフのスイッチ 2 つと、紛れ込んだディレクトリ 1 つが、ファイルがどのメモリパイプラインに入るかを決めます。

目次

10 月 27 日に公開された Codex CLI 0.165.0 のリリースノートは、この変更に 1 行しか割いていません。新規プロファイルでは memory v2 がデフォルトになり、Codex は「external-agent memories」をそこへインポートできる、という内容です。ここでいう外部エージェントとは Claude Code のことです。インポーターは Claude Code がプロジェクトごとに保持している auto memory を読み、Codex のメモリワークスペースにコピーし、Codex が新しいセッションのたびに注入するサマリーへそれを組み込むモデルの処理をキューに入れます。

その 1 行には、フラグと順序のことが書かれていません。インポートはオフで開発中と表示されたフィーチャーフラグの後ろにあるため、標準のインストールでは /import にメモリの項目そのものが出てきません。そして v2 になるかどうかは、最初のインポートがどの設定で動くかで決まります。インポート用のフラグだけをオンにしていると、インポート自体が、プロファイルを v1 に留めるディレクトリを作ってしまいます。

rust-v0.165.0 タグのソースを読み、そのうえで偽の Claude Code プロジェクトを 3 つ置いた使い捨てのホームディレクトリに対して、codex app-server 経由でインポートを実行しました。以下の内容はすべてこの 2 つのどちらかに基づいていて、どちらなのかは都度書きます。

Codex 0.165 は初期設定のままで Claude Code のメモリをインポートできますか?

いいえ。インポートフローのメモリの行は external_agent_memory_import フィーチャーがオンのときにだけ表示され、features/src/lib.rs では Stage::UnderDevelopment、default_enabled: false になっています。メモリ自体は別のフィーチャー memories で、こちらは stable ですが、やはりデフォルトでオフです。

実行してみると、クリーンなプロファイルでの検出結果は最近のチャットセッションだけでした。config.toml にフラグを追加すると、Memory の項目が現れました。どのインポートよりも前に、両方のフィーチャーをまとめて設定してください。

toml
[features]
memories = true
external_agent_memory_import = true

フラグなしでメモリのインポートリクエストを送ると、app server は external agent memory import is disabled で拒否します。TUI の /import では、Memory がホームセクションにチェックボックス 1 つとして表示され、検出された他の項目と一緒に最初からチェックが入っています。

Claude Code のメモリをインポートした後、Codex はどのメモリバージョンを使いますか?

memories.version が未設定の場合、Codex が v2 をデフォルトにするのは、memories フィーチャーがオンで、memories.dual_write が明示的に false になっておらず、v1 のディレクトリである ~/.codex/memories が存在しないときだけです。core/src/config/mod.rs における「新規プロファイルは v2 がデフォルト」の中身はこれで全部です。それ以外の場合は v1 にフォールバックします。memories.version を明示すれば、常にそれが優先されます。

インポーターには独自のルールがあります。~/.codex/memories_v2 には常に書き込み、解決されたバージョンが v1 の場合、またはそのディレクトリがすでに存在する場合は ~/.codex/memories にも書き込みます。多くの人が試すであろう手順は、ここでつまずきます。インポートに必要なのはインポート用のフラグなので、それだけをオンにします。memories がオフなのでバージョンは v1 に解決され、インポートは両方の名前空間に書き込み、~/.codex/memories が存在する状態になります。後から memories をオンにすると、Codex は v1 のワークスペースを見つけて v1 のままにします。

実行結果もそのとおりでした。インポート用のフラグだけを設定した状態では、インポートで memories と memories_v2 の両方が作られました。同じファイルに memories = true を入れた別のクリーンなプロファイルでは、作られたのは memories_v2 だけでした。app-server の統合テストにも同じ組み合わせが書かれていて、「new v2 user」から「legacy workspace」まで 5 ケースあります。

名前とは逆の意味に読める設定が 1 つあります。memories.dual_write = false は、インポートが 2 つのコピーを書くのを止める設定ではありません。v2 デフォルトからのオプトアウトで、新規プロファイルを v1 に留めます。起動時に両方のパイプラインが動く本当の dual-write は version = "v1" かつ dual_write = true のときだけで、dual_write のデフォルトは false です。

[memories] の下で version = "v2" を設定すると、起動時のパイプラインは v2 に固定されます。ただしメモリが有効になるわけではなく、~/.codex/memories が存在すればインポートがそこにも書き込むことも止まりません。すでにそのディレクトリがあるなら、インポートは両方のコピーを最新に保ちます。

Codex のインポーターは Claude Code のメモリフォルダから何をコピーしますか?

各プロジェクトの memory/ ディレクトリ配下にあるすべての Markdown ファイルを、バイト単位でそのままコピーします。Claude Code は auto memory を ~/.claude/projects/<project>/memory/ に保持しています。中身は MEMORY.md というインデックスと、メモリごとに 1 つずつある frontmatter 付きのトピックファイルです。インポーターはこのツリーを再帰的にたどり、拡張子が .md(大文字小文字は問わない)の通常ファイルを取り込み、シンボリックリンクはファイルへのリンクもディレクトリへのリンクも飛ばします。

各プロジェクトは専用のフォルダに入り、ファイルが 1 つ追加されます。

~/.codex/memories_v2/extensions/external_agent_import/
├── instructions.md
└── resources/
    └── -work-app-a/
        ├── scope.json        {"cwd":"/home/you/work/app-a"}
        ├── MEMORY.md
        ├── build.md
        └── notes/deep.md

実行では、コピーされたトピックファイルは cmp で比べて frontmatter までソースと同一で、入れ子の notes/deep.md もパスを保っていました。同じフォルダにあったシンボリックリンクの linked.md は届きませんでした。フォルダ名は Claude Code のプロジェクトキー、つまり ~/.claude/projects の下に見えるエスケープ済みのパスです。MEMORY.md はそのままコピーされ、consolidation の指示は resources/ 配下の編集を禁じています。Claude Code 側には何も書き戻されません。

Codex のインポーターはどの Claude Code プロジェクトをスキップしますか?

作業ディレクトリを特定できないプロジェクトすべてです。Claude Code のメモリフォルダには、どのディレクトリのものかが記録されていません。そのためインポーターはプロジェクトのセッショントランスクリプトを新しい順に読み、最初に使えるものの cwd を採用します。使えるとは、タイムスタンプ付きの会話メッセージがあり、continuation で置き換えられておらず、現在も存在する絶対パスのディレクトリを指している、という意味です。これが scope.json になります。使えるセッションがないプロジェクトは、初回インポートの候補に出てきません。

これはごく普通の 2 つの状況で起きます。Claude Code は cleanupPeriodDays の保持期間を過ぎた古いセッショントランスクリプトを削除しますが、メモリディレクトリは残します。そのため、しばらく開いていないプロジェクトには、メモリがあってもトランスクリプトがないことがあります。また、ディレクトリを移動または削除したプロジェクトのトランスクリプトは、存在しない場所を指しています。用意したフィクスチャにはそれぞれ 1 つずつ入れておきました。どちらも一覧には出ず、リクエストに手で追加すると、結果は両方を失敗として報告しました。

selected memory was not found: -work-app-b
selected memory was not found: -work-app-c

以前にインポートされ、その後トランスクリプトを失ったプロジェクトは、既存のコピーを保持します。インポーターはその更新を飛ばすだけです。

ソースは通常 $HOME/.claude です。インポーターは Claude Code 自身の保存場所の上書き設定を読まないため、autoMemoryDirectory で auto memory を移動していたり、CLAUDE_CONFIG_DIR の下で Claude Code を動かしていたりすると、そこは見に行きません。Claude Code のドキュメントによると、1 つのリポジトリのすべての worktree は 1 つのメモリディレクトリを共有します。インポーターはそのディレクトリを、使える最新のセッションが動いたチェックアウトにスコープするので、メインのリポジトリではなく worktree になることもあります。

次のスクリプトは、このロジックのうち cwd の部分を再現するヒューリスティックで、始める前にどのプロジェクトに使えるディレクトリがあるかを確認できます。正式な一覧は検出が提示するものです。スクリプトは、すでに存在するコピーとの比較はしません。

python
import json, os, pathlib
 
def usable_cwd(session):
    cwd, has_message = None, False
    try:
        lines = session.read_text(errors="ignore").splitlines()
    except OSError:
        return None
    for line in lines:
        try:
            rec = json.loads(line.strip())
        except ValueError:
            continue
        if not isinstance(rec, dict):
            continue
        if rec.get("type") == "continued-in" and rec.get("continuedInSessionId"):
            return None
        if cwd is None and isinstance(rec.get("cwd"), str):
            cwd = rec["cwd"]
        if isinstance(rec.get("message"), dict) and rec.get("timestamp"):
            has_message = True
    if cwd and has_message and os.path.isabs(cwd) and os.path.isdir(cwd):
        return os.path.realpath(cwd)
    return None
 
root = pathlib.Path.home() / ".claude" / "projects"
for proj in sorted(root.iterdir()) if root.is_dir() else []:
    mem = proj / "memory"
    if proj.is_symlink() or mem.is_symlink() or not mem.is_dir():
        continue
    files = [f for f in mem.rglob("*") if f.suffix.lower() == ".md"
             and f.is_file() and not f.is_symlink()]
    sessions = [s for s in proj.glob("*.jsonl") if s.is_file() and not s.is_symlink()]
    sessions.sort(key=os.path.getmtime, reverse=True)
    cwd = next((c for s in sessions if (c := usable_cwd(s))), None)
    print("CWD FOUND" if cwd and files else "NO CWD   ", len(files), proj.name, cwd or "")

フィクスチャでは CWD FOUND が 1 行、NO CWD が 2 行出力され、検出が提示した内容と一致しました。

Codex の consolidation は Claude Code の MEMORY.md をどう扱いますか?

読んだうえで、そこへの道筋を別のファイルに書きます。コピーの後、インポーターは consolidation ジョブをキューに入れます。これは memory_summary.md を書くモデルの処理です。v2 の consolidation プロンプトには、このファイルが「新しいセッションのたびに冒頭へ注入される」と書かれています。バリデーターは、先頭行が v1 でない、10,000 バイト以上ある、または次の見出しのどれかが欠けているサマリーを拒否します。

## User Profile
## User preferences
## General Tips
## What's in Memory

インポーターは instructions.md に独自のルールを加えています。全文を引用した次の 4 項目が、手で整えてきたプロジェクトがどうなるかを決めます。

- For each project, always read its source `MEMORY.md` first when it exists.
  Use it to add or update the project's memory entry in the active pipeline's
  output files.
- Treat every other source `*.md` file as detailed supporting evidence analogous
  to a rollout summary. Do not flatten its full contents into Codex `MEMORY.md`
  or `memory_summary.md`. Keep the detail in the imported resource, add a concise
  pointer in `memory_summary.md` when useful, and read the resource progressively
  when a later task needs that topic.
- In `memory_summary.md`, represent imported project memory only as a compact
  route under `## What's in Memory`. Do not copy its contents into
  `## User Profile`, `## User preferences`, or `## General Tips`, even with a
  project-scope qualifier.
- Imported resources have no rollout `updated_at`. When no reliable source date
  exists, route them under `### Older Memory Topics`; do not invent a date or use
  the consolidation date.

つまりこの指示はモデルに対して、Claude Code の各プロジェクトを、有用だと判断した場合に簡潔な道筋へ変換し、信頼できる日付が見つからない限り Older Memory Topics の下に置くよう求めています。そのすべてが、他のインポート済みプロジェクトや Codex 自身のメモリと、上限 10,000 バイトのサマリー 1 つを取り合います。Claude Code で一度書き留めた好みは、Codex の好みにはなりません。インポートした内容をプロフィールのセクションへ持ち上げることを、ルールが禁じているからです。Claude Code が frontmatter に書く modified タイムスタンプを信頼できる日付として扱うかどうかは、モデル任せです。コードの中にそれをパースする処理はありません。

このルールには筋が通っています。あるリポジトリ用に記録した好みが、コピーされたというだけでグローバルになるべきではありません。ただしその結果、このインポートは仕事のやり方の移行にはなりません。実態は、プロジェクトノートの書庫に、その一部について目録の項目が付いたもので、その項目を書くのはモデルです。関連する議論は Markdown ドキュメントとしてのエージェントメモリで書きました。インデックスこそが検索のレイヤーだ、という話です。ここでは自分のインデックスはディスク上に残り、Codex が実際に読み込むのは、それを他者が要約したものです。

consolidation はメモリの起動パイプラインの中で動き、このパイプラインは memories フィーチャーがオフだと早い段階で return します。インポート用のフラグだけがオンの状態では、ファイルは届いても誰も読みません。consolidation は実行していないので、道筋が実際にどう書かれるかは示せません。この部分はプロンプトとバリデーターから読み取ったもので、観察結果ではありません。

Codex のメモリインポートを再実行するとどうなりますか?

選択したプロジェクトを同期します。インポーターはプロジェクトごとに resources/ 配下のフォルダを削除し、ソースから書き直すので、Claude Code で消したファイルは Codex からも消えます。実行では、偽のプロジェクトから build.md を削除して再インポートすると、両方の名前空間から消えました。検出は Codex がすでに持っているコピーも見ます。Claude Code にもう存在しないプロジェクトのコピーは再び候補に出て、インポートするとそのコピーが削除されます。コピーがすでに同一のプロジェクトは、候補にまったく出てきません。

同期の対象はコピーだけです。Codex が自分のセッションで学んだことは別の場所、つまり rollout summary に保存され、Claude Code に書き戻されることはありません。インポートのたびに新しい consolidation がキューに入り、ソースの変更を共有サマリーに反映し、削除されたファイルだけが裏付けていた記述を落とします。それでも、同じプロジェクトの 2 つのメモリは、Codex の最初のセッションから離れていきます。同じリポジトリで両方のツールを使っているなら(よくある役割分担)、再インポートを定期的に組む前に、どちらのツールのメモリを正とするかを決めておいてください。

Codex のメモリインポートが「memory workspace is busy」で失敗するのはなぜですか?

consolidation か別のインポートが、ワークスペースのロックを握っているからです。各名前空間の隣にはロックファイル ~/.codex/memories_v2.lock と ~/.codex/memories.lock があり、インポートは何かを変更する前に、必要なロックをすべて取得します。どれか 1 つでも握られていれば、次のメッセージで止まります。

memory workspace is busy; retry after consolidation or another import finishes

その時点では何も書き込まれていないので、中途半端にインポートされた v1 のコピーを片付ける必要はありません。consolidation が終わってから再試行してください。プルリクエスト #56462 で、インポートと consolidation の競合状態が解消されました。

Claude Code のメモリを Codex にインポートする前に確認すること

まずはスクラッチのプロファイルで試してください。CODEX_HOME を空のディレクトリに向け、そのディレクトリの config.toml に両方のフィーチャーを書き、使い捨ての作業ディレクトリから codex を起動します。他のインポート項目が、現在のプロジェクトにファイルを追加することがあるためです。/import で Customize selection を選び、Memory だけにチェックを残します。そのうえで、本番のプロファイルで同じことをする前に、memories_v2/extensions/external_agent_import/resources/ に何が入ったかを確認します。インポートは ~/.claude を読むだけで、変更はしません。

本番のプロファイルでは、次の 3 点を確認してください。

  1. 最初のインポートより前に、memories = true とインポート用のフラグを config.toml に入れておきます。~/.codex/memories がすでに存在するなら、自動で v2 になる道はありません。それでも v2 パイプラインを使いたければ memories.version = "v2" を設定します。
  2. 上のスクリプトを実行します。NO CWD の行はどれも、メモリが移行されないプロジェクトです。たいていはトランスクリプトがクリーンアップされたことが原因です。そのプロジェクトで Claude Code の会話を始めると、実在する cwd を持つ新しいトランスクリプトが書かれます。
  3. 承認する前に、プロジェクトの一覧を全部読みます。TUI が表示するのは件数と最初の 4 つのプロジェクトキーだけで、チェックボックス 1 つで、検出されたすべてのプロジェクトを 1 つのプロファイル、1 つのサマリーにインポートします。あるプロジェクトのノートを別のプロジェクトのセッションに混ぜないための仕組みは、scope.json と指示の中の 1 文だけです。他人のプロジェクトが含まれている場合や、単にサマリーを共有すべきでない場合は、それらに別の CODEX_HOME を使うか、プロジェクトキーを明示的なリストで受け取る app-server API を使ってください。

このインポートでのデフォルトは、オフになっているスイッチ 2 つと、まだ存在してはいけないディレクトリ 1 つです。

01 / ディスカッション

コメント欄はありません。議論は X で行っています。

Max Nardit@mnardit

02 / ほかの記事

  1. GPT-6.1 Terra をコンピューター操作に使うなら: エージェント構成ではレビュー役よりワーカー役に向く

    中位モデルが価格に見合うのは、実際に影響が出る前に結果を安く確認できる場面です。Terra のベンチマークが測るのはタスクあたりのコストですが、コンピューター操作で決め手になるのは誤クリック 1 回の代償で、OpenAI のグラフにはその軸がありません。

  2. OpenAI のフロンティア学習にインターネットを使わないルール:外への出口になったのは許可されたサービスで、エージェントのサンドボックスはこの分離をまねるべき

    Web のコピーは持ちこたえました。OpenAI 自身のレッドチームが抜け道を見つけたのは、ラボの運用のために接続を残したサービスのほぼすべてで、9 月の脱出はどのコピーも置き換えていなかったリゾルバを通っていました。エージェントの作業はコピーでまかない、ライブ接続は少なくしてエージェントの手の届かない場所に置き、そのコストは github.com に集中すると考えておくことです。

  3. Vox の AGENTS.md の行を Claude Code で 90 回試した: いちばん効いた行は彼のものではなかった

    1 行目を入れるとエージェントは入力チェックを省きました。2 行目が足した要素は、1 行目と同じファイルに並ぶと消えました。「without breaking anything」は、テストコマンドを書いた行がすでに果たしていた以上の効果をはっきりとは示しませんでした。90 回の実行で言えること、言えないこと、そして行を残す前に試す方法。