コンテンツにスキップ

OpenAI / ChatGPT ガイドハブ

CodexをWSLからWindowsへ移行し、消えたサイドバー履歴を復元する

復元できた範囲と重要な制約

WSLからWindowsネイティブ版Codexへ切り替えた検証環境で、通常セッションとアーカイブを合わせて1,000件超の索引を復元し、サイドバーから再び開ける状態まで戻した。

  • WSL、旧Windows CLI、現Windows版の保存領域を別々にバックアップする
  • state_*.sqlite、認証情報、設定は混ぜず、rollout-*.jsonlだけを移す
  • スレッドIDを直接指定し、移行したJSONLを1件だけ検証する
  • Codexのapp-server APIで通常スレッドとアーカイブの索引を再構築する

この手順はCodex CLI 0.144.4でコマンドとJSON Schemaを再確認した。 app-serverは実験的インターフェースであるため、別バージョンでは必ず同梱Schemaを生成してから使う。

Codexのサイドバーから過去のスレッドが消えても、会話本文が削除されたとは限らない。WSLとWindowsが別のCODEX_HOMEを参照し、Windows側の状態DBに旧セッションの索引がないだけの場合がある。

この記事は、実際に分散したセッションを統合し、通常スレッドとアーカイブのサイドバー索引まで復元した作業記録を、公開用のプレースホルダーへ置き換えたものだ。個人名、ユーザー名、スレッドID、プロジェクト名、保存ドライブ構成は含めていない。

WSLからWindowsへCodex履歴を安全に移行する流れ

履歴消失の多くは保存領域と索引の分離で起きる

Codexはローカル状態をCODEX_HOME配下に保存する。公式のConfiguration Referenceは、ここに設定、認証、ログ、セッションなどが含まれると説明している。1

Windows版へ切り替えた環境では、次の3領域に別々のセッションが残ることがある。

保存領域パスの例
WSL側/home/<WSL_USER>/.codex
旧Windows CLI側C:\Users\<WINDOWS_USER>\.codex
現在のWindows版C:\Users\<WINDOWS_USER>\AppData\Local\OpenAI\CodexAppHome

Windows版の保存先を固定値で決めつけてはいけない。 公式のWindows向け資料は%USERPROFILE%\.codexを案内しているが、 アプリの配布形態や移行状態によって別の場所が使われる環境もある。 実際の保存先は後述するcodex doctorで確認する。2

会話実体と一覧索引は別の層にある。

主な内容移行時の扱い
rollout-*.jsonl会話、ツール実行、セッションメタデータバックアップ後に無上書きで統合
state_*.sqliteスレッド一覧などの状態DBOS間でコピーしない
auth.jsonまたはキーチェーン認証情報移行対象にしない
config.tomlローカル設定環境ごとに管理
.codex-global-state.jsonアプリUIの状態移行対象にしない

OpenAIのヘルプも、ローカルで開始したチャットはそのコンピューターに残り、Codexの履歴はChatGPTの通常履歴と分かれていると説明している。3

最初にCodexと3つの保存領域を停止・特定する

バックアップと索引処理の前に、Codex/ChatGPTデスクトップアプリとCodex CLIを終了する。PowerShellでは関連プロセスを確認できる。

Get-Process -Name Codex,ChatGPT -ErrorAction SilentlyContinue |
  Select-Object ProcessName, Id, StartTime

次に、Windowsネイティブのcodexが実際に使っている状態を診断する。

codex --version
codex doctor --summary --no-color --ascii

機械可読な診断結果が必要なら、マスク済みJSONをローカルだけで確認する。

$doctor = codex doctor --json | ConvertFrom-Json
$doctor.checks | Format-List

doctorのJSON構造はバージョンで変わり得る。フィールド名を決め打ちした移行スクリプトを先に作らず、現在の出力からCODEX_HOME、状態DB、rolloutとDBの差分を確認する。

診断結果もそのまま公開しない

codex doctor --jsonは診断情報をマスクするが、ローカルパスやサンプル値が含まれる可能性がある。Issueやブログへ貼る前に、ユーザー名、プロジェクト名、スレッドIDを再確認する。

バックアップはOSと世代を混ぜずに取得する

WSL、旧Windows CLI、現Windows版の保存領域を、それぞれ別のフォルダーへ丸ごと退避する。バックアップ先は元データと別のドライブが望ましい。

$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$BackupRoot = "E:\Codex-Migration-Backups\$stamp"

New-Item -ItemType Directory -Path $BackupRoot -Force | Out-Null

robocopy `
  $WslCodexHome `
  (Join-Path $BackupRoot "wsl-native\.codex") `
  /E /COPY:DAT /DCOPY:DAT /R:1 /W:1

if ($LASTEXITCODE -ge 8) {
  throw "WSL側バックアップに失敗: robocopy exit code $LASTEXITCODE"
}

robocopy `
  $WindowsCodexHome `
  (Join-Path $BackupRoot "windows-current\CodexHome") `
  /E /COPY:DAT /DCOPY:DAT /R:1 /W:1

if ($LASTEXITCODE -ge 8) {
  throw "Windows側バックアップに失敗: robocopy exit code $LASTEXITCODE"
}

robocopyは終了コード0〜7を成功または差分ありとして使う。 8以上を失敗として扱う。 旧Windows CLI側にも独自セッションがある場合は、3つ目のフォルダーへ分けて保存する。

障害復旧中はWSLとWindowsでSQLiteを共有しない

公式のWindows向け資料は、WSL側のCODEX_HOMEをWindows側へ向け、 設定、認証、セッションを共有する方法も案内している。 正常に動く構成で、この方法自体を否定するものではない。2

一方、公式リポジトリのIssue #23251では、 WSL CLIからWindows版のCODEX_HOMEを開いた際に、 SQLiteのマイグレーション不整合で起動できない事例が報告されている。 コピーしたDBのPRAGMA integrity_checkokでも、 ランタイム間のマイグレーションチェックに失敗した。4

この記事の検証環境でも、復旧作業では次を分離した。

  • Windows版はWindows側のCODEX_HOMEと状態DBを使う
  • WSL版はWSL側のCODEX_HOMEと状態DBを使う
  • OS間で移すのはバックアップ済みのrollout-*.jsonlだけ
  • SQLiteへINSERTUPDATE、スキーマ変更を行わない

これは通常運用の一般ルールではなく、履歴復旧時に既存状態を壊さないための安全境界である。

セッション実体はスレッドIDで無上書き統合する

移行前に各保存領域のJSONL件数を数える。

foreach ($home in @(
  $WslCodexHome,
  $LegacyWindowsCodexHome,
  $WindowsCodexHome
)) {
  $active = @(
    Get-ChildItem (Join-Path $home "sessions") `
      -Recurse -Filter "rollout-*.jsonl" `
      -ErrorAction SilentlyContinue
  ).Count

  $archived = @(
    Get-ChildItem (Join-Path $home "archived_sessions") `
      -Recurse -Filter "rollout-*.jsonl" `
      -ErrorAction SilentlyContinue
  ).Count

  [pscustomobject]@{
    CodexHome = $home
    Active = $active
    Archived = $archived
    Total = $active + $archived
  }
}

rollout-*.jsonlが存在すれば、少なくともセッション実体が残っている可能性がある。ただし、空ファイル、古い形式、内部実行などが混ざるため、件数だけで復元成功とは判断しない。

ファイルパスや日付フォルダーではなく、JSONLファイル名末尾のUUIDをスレッドIDとして重複判定する。Windows側の通常・アーカイブ両方に存在しないIDだけをコピーする。

無上書き統合用PowerShell

$knownIds = [System.Collections.Generic.HashSet[string]]::new(
  [System.StringComparer]::OrdinalIgnoreCase
)

foreach ($destination in @(
  (Join-Path $WindowsCodexHome "sessions"),
  (Join-Path $WindowsCodexHome "archived_sessions")
)) {
  Get-ChildItem $destination -Recurse -Filter "rollout-*.jsonl" `
    -ErrorAction SilentlyContinue |
    ForEach-Object {
      if ($_.Name -match
          '([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\.jsonl$') {
        [void] $knownIds.Add($Matches[1])
      }
    }
}

function Copy-CodexRolloutsWithoutOverwrite {
  param(
    [Parameter(Mandatory)] [string] $SourceRoot,
    [Parameter(Mandatory)] [string] $DestinationRoot,
    [Parameter(Mandatory)] $KnownIds
  )

  if (-not (Test-Path -LiteralPath $SourceRoot)) {
    return
  }

  New-Item -ItemType Directory -Path $DestinationRoot -Force |
    Out-Null

  $copied = 0
  $skipped = 0
  $sourcePrefixLength = $SourceRoot.TrimEnd('\').Length

  Get-ChildItem $SourceRoot -Recurse -Filter "rollout-*.jsonl" |
    ForEach-Object {
      if ($_.Name -notmatch
          '([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\.jsonl$') {
        return
      }

      $threadId = $Matches[1]
      if ($KnownIds.Contains($threadId)) {
        $skipped++
        return
      }

      $relativePath = $_.FullName.Substring(
        $sourcePrefixLength
      ).TrimStart('\')
      $targetPath = Join-Path $DestinationRoot $relativePath
      $targetDirectory = Split-Path $targetPath -Parent

      New-Item -ItemType Directory -Path $targetDirectory -Force |
        Out-Null
      Copy-Item -LiteralPath $_.FullName -Destination $targetPath

      [void] $KnownIds.Add($threadId)
      $copied++
    }

  [pscustomobject]@{
    Source = $SourceRoot
    Destination = $DestinationRoot
    Copied = $copied
    SkippedExistingThreadId = $skipped
  }
}

Copy-CodexRolloutsWithoutOverwrite `
  -SourceRoot (Join-Path $WslCodexHome "sessions") `
  -DestinationRoot (Join-Path $WindowsCodexHome "sessions") `
  -KnownIds $knownIds

Copy-CodexRolloutsWithoutOverwrite `
  -SourceRoot (Join-Path $WslCodexHome "archived_sessions") `
  -DestinationRoot (Join-Path $WindowsCodexHome "archived_sessions") `
  -KnownIds $knownIds

旧Windows CLI側にも独自セッションがある場合は、同じ関数を追加で呼ぶ。同じスレッドIDがWindows側の通常・アーカイブのどちらかにあれば上書きしない。

移行したJSONLは重要度の低い1件で直接検証する

サイドバーへ出なくても、スレッドIDを直接指定すると開ける場合がある。

codex resume <THREAD_ID>

codex resume --allが既存JSONLを一覧に出さない一方、IDを直接指定すると再開できる事例は、公式リポジトリのIssue #20165でも報告されている。5

最初は重要度の低い1件で実行し、履歴が開いたらプロンプトを送信せず終了する。 これで、少なくとも次の2点を分けて判断できる。

  1. Windows版Codexが移行後のJSONLを読める
  2. サイドバーに出ない原因は、会話実体より一覧索引側にある

サイドバー索引はapp-serverで再構築する

JSONLをコピーしただけでは、Windows側の状態DBにスレッド行がなく、 サイドバーへ表示されない場合がある。 Issue #34782でも、セッション実体が残っているのにサイドバー履歴が消え、 アプリ再起動で再索引されなかったWindows/WSL事例が報告された。6

検証環境では、Codexのapp-server APIで各スレッドをthread/resumeし、 通常スレッドの索引を再構築した。 これは推測上の回避策ではなく、1,000件超の復元で実際に使った処理である。

実行中のCodexに一致するSchemaを生成する

app-serverのREADMEは、実行中のCodexバージョンに一致するJSON Schemaを生成できると説明している。7

codex app-server generate-json-schema `
  --experimental `
  --out .\codex-app-server-schema

Schemaで少なくとも次を確認する。

  • thread/resumeの必須フィールド
  • excludeTurnsが利用できるか
  • thread/archivethread/unarchiveの引数
  • initialize.params.clientInfoの必須フィールド
  • 実験的APIの有効化方法

1件だけメタデータ中心に再開する

Codex CLI 0.144.4で確認した最小メッセージは次の形になる。

$threadId = "<THREAD_ID>"

$messages = @(
  @{
    id = 1
    method = "initialize"
    params = @{
      clientInfo = @{
        name = "sidebar-reindex-test"
        title = "Sidebar reindex test"
        version = "1.0.0"
      }
      capabilities = @{
        experimentalApi = $true
      }
    }
  },
  @{
    method = "initialized"
    params = @{}
  },
  @{
    id = 2
    method = "thread/resume"
    params = @{
      threadId = $threadId
      excludeTurns = $true
    }
  },
  @{
    id = 3
    method = "thread/unsubscribe"
    params = @{
      threadId = $threadId
    }
  }
)

$messages |
  ForEach-Object { $_ | ConvertTo-Json -Depth 10 -Compress } |
  codex app-server --listen stdio://

excludeTurns = $trueは、会話全体をレスポンスへ展開せず、 メタデータと再開状態を返すために使う。 処理後はcodex doctorとサイドバーの両方で、 対象スレッドが索引化されたか確認する。

一括処理はレスポンス待機を実装する

上のパイプラインは1件の疎通確認用である。 複数IDを処理するクライアントは、initialize完了、各thread/resume応答、 thread/unsubscribe応答を順番に待つ必要がある。 成功・失敗を逐次ログへ保存し、タイムアウト時はapp-serverだけを再起動する。 全IDを応答待ちなしで標準入力へ流さない。

実際の一括復元では、次の安全策を入れた。

  1. 処理側がWindows版のCODEX_HOMEを確認する
  2. sessionsからスレッドIDを列挙する
  3. thread/resumeを1件ずつ送る
  4. 各レスポンス後にthread/unsubscribeする
  5. タイムアウトしたIDを記録し、app-serverを再起動する
  6. 成功・失敗をJSONLログへ逐次保存する
  7. SQLiteへ直接書き込まない
  8. 最後にcodex doctorで差分を確認する

アーカイブ索引は解除と再アーカイブで復元する

アーカイブJSONLは、コピーしただけではアーカイブ一覧へ出ない場合がある。検証環境では、各スレッドを一度解除して再アーカイブすると、JSONLを最終的にアーカイブ位置へ戻したまま、状態DBのアーカイブ行を再生成できた。

安定CLIが使える現在のバージョンでは、まず次を使う。

codex unarchive <THREAD_ID>
codex archive <THREAD_ID>

app-serverクライアントへ組み込む場合のメソッドは次の形になる。

{"id":10,"method":"thread/unarchive","params":{"threadId":"<THREAD_ID>"}}
{"id":11,"method":"thread/archive","params":{"threadId":"<THREAD_ID>"}}

必ずバックアップ後に重要度の低い1件で試し、次を確認してから一括処理へ進む。

  • sessions側に同じIDが重複して残っていない
  • archived_sessions側にJSONLが1件ある
  • codex doctorでアーカイブ不整合が増えていない
  • Windows版からアーカイブ一覧を開ける

全JSONL数とサイドバー件数は一致しない

Codexのthread/listは、sourceKindsを省略すると対話的なソースを既定対象にする。 生成済みSchemaでは、clivscodeexecappServer、 複数のサブエージェント種別などが区別されている。 内部実行やサブエージェントのJSONLまで、 すべて通常サイドバーへ並ぶわけではない。7

したがって、全JSONL数 > 状態DBの有効行数 > 通常サイドバーへ表示される対話スレッド数 という大小関係は失敗を意味しない。

復元判定では、件数だけでなく次を見る。

  1. WSL由来の既知スレッドIDを直接再開できる
  2. そのIDがthread/listまたはサイドバーへ返る
  3. 通常セッションとアーカイブの所属が正しい
  4. codex doctorで状態DBの整合性が正常である

検証環境では、通常セッションとアーカイブを合わせて1,000件超を索引化した。 missing active rowsmissing archived rowsstale rowsarchive mismatchesはすべて0件になった。

古いJSONLの一部はno parseable rollout itemsと判定された。 該当ファイルは削除せずバックアップへ残し、 現在のCodexが読めるセッションだけを索引化した。

公開ログからセッション内容と秘密情報を除く

ブログ、GitHub Issue、Gistへログやコマンドを載せる前に、次を置換する。

公開しない値置換後
Windowsユーザー名<WINDOWS_USER>
WSLユーザー名<WSL_USER>
WSLディストリビューション<DISTRO>
プロジェクト名<PROJECT>
スレッドID<THREAD_ID>
ローカル絶対パス<LOCAL_PATH>
メールアドレス<EMAIL>
APIキー・トークン<REDACTED>
組織名・リポジトリ名<REPOSITORY>

次のファイルは内容をそのまま公開しない。

  • auth.json
  • config.toml
  • .codex-global-state.json
  • rollout-*.jsonl
  • logs_*.sqlite
  • state_*.sqlite

JSONLには会話本文、コマンド、ファイルパス、プロジェクト情報が含まれ得る。OpenAIの公式資料も、ログを共有する前に機密情報がないか確認するよう案内している。8

復元完了はCodex自身の診断と実表示で判定する

最後にWindowsネイティブ側で診断する。

codex doctor --summary --no-color --ascii

復元完了の判定は、次の順に行う。

  1. Windows版Codexが状態DBを正常と診断する
  2. 移行した通常スレッドをサイドバーから開く
  3. 移行したアーカイブをアーカイブ一覧から開く
  4. 既存のWindows側スレッドが上書きされていない
  5. 既知のWSL側スレッドIDを直接再開できる
  6. scan errorが残るJSONLを削除せず、別途記録する
  7. 元のWSLデータとバックアップを当面保持する

この復元では、会話実体、状態DB、サイドバー表示を別々に検証する。 JSONLをコピーできただけでも、ファイル数が一致しただけでも完了ではない。 Windows版Codexが実際に一覧へ出し、 スレッドを開けるところまで確認して初めて復元済みと判断できる。

関連記事