Codex Desktop: chats disappear after update/import; imported threads open blank despite JSONL turns

Open 💬 8 comments Opened Apr 30, 2026 by vishkrish200
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

After updating Codex Desktop and using the Imported agent setup flow, existing local chats appeared to disappear from the Desktop sidebar. The transcript JSONL files were still present on disk and could be recovered manually, but the Desktop app no longer surfaced them normally. Some imported/external chats also open to a blank-looking chat view from codex://threads/<id> even though the app-server can parse the transcript when turns are explicitly requested.

This presents to the user as data loss, even if the raw transcripts still exist.

Related/similar issues I found before filing:

  • #18364
  • #17540
  • #19345
  • #19615
  • #19873
  • #20419
  • #14370

Environment

  • Codex CLI/Desktop bundle: codex-cli 0.128.0-alpha.1
  • macOS: 26.4.1 25E253
  • Architecture: arm64
  • Codex Desktop source: installed app bundle at /Applications/Codex.app

What happened

  1. Updated Codex Desktop.
  2. Used the app's "Imported agent setup" flow.
  3. After that, older chats were no longer visible in the Desktop sidebar.
  4. Restarting Codex Desktop did not restore them.
  5. Rebuilding session_index.jsonl and project/root state made the CLI able to browse/resume sessions, but Desktop still did not reliably display/open imported chats.
  6. A generated local page with codex://threads/<id> links could navigate into Codex, but the opened chat view was blank for imported chats.

Evidence

The old transcript files still existed under ~/.codex/sessions/..., so the user data was not fully gone from disk.

However, ~/.codex/state_5.sqlite was corrupt immediately after the issue:

  • file ~/.codex/state_5.sqlite reported generic data, not SQLite.
  • App logs contained errors such as:
  • failed to initialize sqlite state db: file is not a database
  • sqlite state db unavailable for thread ...

After moving the corrupt DB aside, Codex rebuilt a valid SQLite DB. The rebuilt DB currently contains:

  • 210 total threads
  • 200 unarchived threads
  • 113 threads with source vscode

The import registry exists and shows external sessions were imported:

  • ~/.codex/external_agent_session_imports.json
  • Contains 72 imported external session records.

Example imported thread:

  • Thread id: 019ddfce-b73e-7230-ae24-a70b89c76982
  • Transcript path shape: ~/.codex/sessions/2026/05/01/rollout-...-019ddfce-b73e-7230-ae24-a70b89c76982.jsonl
  • thread/read without includeTurns:true returned metadata/preview but turns: [].
  • thread/read with includeTurns:true returned the actual user and assistant messages.

Desktop logs around opening imported chats included:

  • Conversation state not found conversationId=...
  • followed by maybe_resume_success ... turnCount=1

Despite that, the Desktop UI appeared blank.

There was also repeated log noise for at least one imported thread with cwd /:

  • No cwd found for local task conversationId=019ddfce-17b1-7893-a39c-d66ace3561f6

Expected behavior

  • App updates and import flows should not make existing chats disappear from the Desktop sidebar.
  • If state_5.sqlite migration/open fails, Desktop should detect the corruption, preserve the DB, rebuild from JSONL, and show a visible recovery/warning state instead of looking like the user's chats are gone.
  • Imported external sessions should either render correctly in Desktop or be clearly labeled as imported/archive-only with a built-in viewer/export.
  • codex://threads/<id> should not navigate to a blank chat when the app-server can parse the transcript.

Actual behavior

  • Existing chats appeared deleted from the Desktop UI.
  • Imported threads could be indexed and listed, but opening them from Desktop/deep link could produce a blank chat.
  • Manual recovery was required to rebuild state and create a separate local viewer for the JSONL transcripts.

Local workaround used

  • Backed up and moved aside the corrupt state_5.sqlite.
  • Allowed Codex to rebuild a valid SQLite DB from local session files.
  • Built a separate local HTML viewer directly from the JSONL transcripts to make the recovered chats readable.

I can provide sanitized logs or additional command output if useful.

View original on GitHub ↗

8 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #20419
  • #19873
  • #19290
  • #19615
  • #20187

Powered by Codex Action

vishkrish200 · 2 months ago

Recovery note: chats existed on disk, but Desktop hid them

I was able to recover the missing sidebar chats locally. Sharing the exact pattern in case it helps others debug the same failure mode.

What was happening

The rollout JSONL files were still present under ~/.codex/sessions/..., and the rows still existed in ~/.codex/state_5.sqlite, but Codex Desktop did not show them in the sidebar.

The decisive clue was that some rows looked like real chats but had sidebar-breaking metadata:

| Field | Bad value I saw | Why it matters |
| --- | --- | --- |
| archived | 0 | The row was active, so this alone was not the issue. |
| first_user_message | non-empty | The chat had a real user prompt/history. |
| has_user_event | 0 | Desktop did not treat the row as a normal visible chat. |
| source | exec for some chat-like rows | These did not behave like interactive sidebar conversations. |

After fixing those fields for active chats, the sidebar started showing the recovered chats again.

Safety first

Please back up before changing anything:

backup_dir="$HOME/.codex/chat-metadata-repair-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$backup_dir"

sqlite3 "$HOME/.codex/state_5.sqlite" ".backup '$backup_dir/state_5.before.sqlite'"
cp "$HOME/.codex/.codex-global-state.json" "$backup_dir/codex-global-state.before.json"

I also recommend not unarchiving everything blindly. In my case, I had intentionally archived many old chats, so the repair below only touches active rows.

Diagnose

sqlite3 "$HOME/.codex/state_5.sqlite" "pragma integrity_check;"

Then look for active rows that have a first user message but bad visibility metadata:

sqlite3 "$HOME/.codex/state_5.sqlite" \
  "select cwd, source, has_user_event, count(*)
   from threads
   where archived=0
     and first_user_message <> ''
     and (has_user_event=0 or source='exec')
   group by cwd, source, has_user_event
   order by count(*) desc, cwd;"

If this returns real project roots/chats, you may be hitting the same bug.

Repair that worked for me

sqlite3 "$HOME/.codex/state_5.sqlite" "
begin immediate;

update threads
set has_user_event=1
where archived=0
  and first_user_message <> ''
  and has_user_event=0
  and source in ('vscode', 'cli', 'exec');

update threads
set source='vscode'
where archived=0
  and first_user_message <> ''
  and source='exec';

commit;

pragma wal_checkpoint(full);
pragma integrity_check;
"

After that, refresh/restart Codex Desktop. For me, a normal UI refresh was enough after the metadata was repaired.

<details>
<summary>Optional: project/sidebar grouping cleanup</summary>

If chats exist but still appear under the wrong project, also inspect:

  • thread-workspace-root-hints
  • project-order
  • electron-saved-workspace-roots

These live in:

~/.codex/.codex-global-state.json

In my recovery, adding explicit thread_id -> cwd hints for active visible threads helped the sidebar group the recovered chats under the expected project roots.

</details>

Why I think this is an import/update bug

The affected rows had real rollout history and a non-empty first_user_message, but were missing the metadata Desktop appears to use for normal chat visibility. That suggests the data was not truly gone; it was present but no longer shaped like a visible interactive thread after the update/import path.

matthewkeen · 2 months ago

Same symptom on the Windows Desktop build (OpenAI.Codex_26.429.3425.0_x64__2p2nqsd0c76g0), but the underlying filter appears to be different.

After a Codex Desktop update on Windows, ~130 of my chats disappeared from the sidebar even though their rollout JSONLs in ~/.codex/sessions/ and rows in state_5.sqlite (threads table) were intact. The disappeared chats were all older than roughly 30 days.

Things that did NOT restore visibility on Windows

Each tried in isolation, with full SQLite backups + integrity_check between attempts:

  • Rebuilding session_index.jsonl to include all rows from threads (Codex Desktop appears to ignore added entries here for sidebar purposes).
  • The UPDATE threads SET has_user_event=1 WHERE archived=0 AND first_user_message<>'' fix from @vishkrish200's comment. Update applied cleanly (213 rows touched, integrity_check ok) but had zero effect on sidebar visibility.
  • UPDATE threads SET source='vscode' WHERE source='exec' (same comment).
  • Cloning thread_dynamic_tools rows from a visible chat to a hidden one (matching tool counts, namespaces, schemas).
  • Bumping threads.updated_at and threads.updated_at_ms to the current epoch.
  • Adding thread-workspace-root-hints entries in ~/.codex/.codex-global-state.json.

What did work

os.utime(rollout_path, (atime, now)) on the disappeared chat's rollout JSONL. The file's content stays bit-identical (verified by SHA256 before/after); only the filesystem mtime changes. After restarting Codex Desktop, the chat reappears in the correct sidebar folder.

Repro

  1. Pick any thread whose rollout_path exists on disk and whose file mtime is more than ~30 days old. It will not be in the sidebar regardless of has_user_event, archived, source, tools count, etc.
  2. os.utime(rollout_path, (atime, time.time())). Restart Codex Desktop.
  3. Chat reappears under the matching workspace-root folder.

Cutoff observation

Visible chats had rollout mtime ≤ 22.5 days; hidden ones ≥ 37 days. The filter sits somewhere in that 22–37 day window — likely 30 days hardcoded.

Separate caveat (sidebar-grouping, not visibility)

With both \\?\W:\X and W:\X for the same logical path in electron-saved-workspace-roots, sidebar matching breaks even for chats that satisfy the recency filter — folders show "No chats" until the duplicate is removed. Keep one form per logical path. Mentioning in case it's related.

Versions

  • Codex Desktop (Windows MSIX): OpenAI.Codex_26.429.3425.0_x64__2p2nqsd0c76g0
  • Affected chats span cli_version 0.101.00.126.0-alpha.8
  • OS: Windows 11 Pro 26200

Repro / repair script

Single-file Python 3.8+ script with diagnose, touch, and restore subcommands. Reads state_5.sqlite read-only; the only write operation is os.utime() on rollout JSONL files. Content is SHA256-verified before and after every touch.

#!/usr/bin/env python3
"""
codex_sidebar_repair.py — workaround for Codex Desktop sidebar hiding chats
whose rollout JSONL filesystem mtime is older than ~30 days.

Usage:
  python codex_sidebar_repair.py diagnose
  python codex_sidebar_repair.py touch [--dry-run]
  python codex_sidebar_repair.py restore <manifest.json>

Set CODEX_HOME env var to override the default ~/.codex location.
"""
import sqlite3, os, json, hashlib, time, sys
from pathlib import Path
from datetime import datetime

CODEX_HOME = Path(os.environ.get('CODEX_HOME', str(Path.home() / '.codex')))
DB = CODEX_HOME / 'state_5.sqlite'
GS = CODEX_HOME / '.codex-global-state.json'
CUTOFF_DAYS = 25  # touch files older than this; observed sidebar cutoff is ~30 days

def to_uri_ro(p):
    s = str(p).replace('\\', '/')
    if len(s) > 1 and s[1] == ':': s = '/' + s
    return 'file:' + s + '?mode=ro'

def normalize_root(p):
    if not isinstance(p, str): return ''
    if p.startswith('\\\\?\\'): p = p[4:]
    return p.lower() if os.name == 'nt' else p

def sha256_file(path):
    h = hashlib.sha256()
    with path.open('rb') as f:
        for chunk in iter(lambda: f.read(65536), b''): h.update(chunk)
    return h.hexdigest()

def query_threads():
    conn = sqlite3.connect(to_uri_ro(DB), uri=True)
    conn.row_factory = sqlite3.Row
    rows = conn.execute("""
        SELECT id, title, cwd, rollout_path FROM threads
        WHERE archived=0 AND has_user_event=1 AND first_user_message<>''
          AND (title IS NULL OR title NOT LIKE '# Codex Task%')
        ORDER BY updated_at_ms ASC
    """).fetchall()
    conn.close()
    return [dict(r) for r in rows]

def diagnose():
    now = time.time()
    visible = hidden = 0
    samples_hidden = []
    for r in query_threads():
        p = Path(r['rollout_path'])
        if not p.exists(): continue
        age = (now - p.stat().st_mtime) / 86400
        if age <= CUTOFF_DAYS: visible += 1
        else:
            hidden += 1
            if len(samples_hidden) < 10:
                samples_hidden.append((age, r['id'], r['title'] or ''))
    print(f"Visible (rollout mtime <= {CUTOFF_DAYS}d): {visible}")
    print(f"Hidden  (rollout mtime >  {CUTOFF_DAYS}d): {hidden}")
    for age, tid, t in samples_hidden:
        print(f"  {age:6.1f}d  {tid[:8]}  {t[:60]!r}")
    if hidden > 10: print(f"  ... and {hidden-10} more")

def touch(dry_run):
    gs = json.loads(GS.read_text(encoding='utf-8'))
    ws_norm = {normalize_root(r) for r in gs.get('electron-saved-workspace-roots', [])}
    now = time.time()
    cutoff = now - CUTOFF_DAYS * 86400
    cands = []
    for r in query_threads():
        if normalize_root(r['cwd']) not in ws_norm: continue
        p = Path(r['rollout_path'])
        if not p.exists() or p.stat().st_mtime > cutoff: continue
        cands.append((r['id'], r['title'], p))
    cands.sort(key=lambda c: c[2].stat().st_mtime)
    n = len(cands)
    print(f"{'[dry-run] ' if dry_run else ''}Will touch {n} rollout files")
    if dry_run or n == 0:
        for tid, t, p in cands[:10]:
            age = (now - p.stat().st_mtime) / 86400
            print(f"  {age:6.1f}d  {tid[:8]}  {(t or '')[:60]!r}")
        return
    out = Path.cwd() / f"codex-touch-{datetime.now().strftime('%Y%m%dT%H%M%S')}"
    out.mkdir(parents=True, exist_ok=True)
    manifest = []
    for i, (tid, title, p) in enumerate(cands):
        st = p.stat()
        sha = sha256_file(p)
        new_mt = now - (3600 - 3540.0 * i / max(1, n - 1))  # spread oldest=60min ago, newest=1min ago
        os.utime(p, (st.st_atime, new_mt))
        if sha256_file(p) != sha:
            sys.exit(f"CONTENT CHANGED on {tid} — aborting")
        manifest.append({
            'thread_id': tid, 'title': title, 'rollout_path': str(p),
            'original_atime': st.st_atime, 'original_mtime': st.st_mtime,
            'new_mtime': new_mt, 'sha256': sha, 'size': st.st_size,
        })
    (out / 'touch_manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8')
    print(f"\nTouched {n} files. Manifest: {out / 'touch_manifest.json'}")
    print(f"Restore: python {sys.argv[0]} restore '{out / 'touch_manifest.json'}'")

def restore(manifest_path):
    manifest = json.loads(Path(manifest_path).read_text(encoding='utf-8'))
    ok = mismatch = missing = 0
    for e in manifest:
        p = Path(e['rollout_path'])
        if not p.exists(): missing += 1; continue
        if sha256_file(p) != e['sha256']:
            mismatch += 1
            print(f"  content mismatch, skipping {e['thread_id']}", file=sys.stderr)
            continue
        os.utime(p, (e['original_atime'], e['original_mtime']))
        ok += 1
    print(f"Restored {ok} of {len(manifest)} (missing={missing}, mismatch={mismatch})")

if __name__ == '__main__':
    if len(sys.argv) < 2 or sys.argv[1] not in ('diagnose', 'touch', 'restore'):
        print(__doc__); sys.exit(1)
    cmd = sys.argv[1]
    if cmd == 'diagnose': diagnose()
    elif cmd == 'touch': touch('--dry-run' in sys.argv)
    elif cmd == 'restore':
        if len(sys.argv) < 3: sys.exit("usage: restore <manifest.json>")
        restore(sys.argv[2])

Quit Codex Desktop before running touch. The script:

  • Skips archived=1 rows (preserves your archived chats).
  • Skips rows with cwd not in electron-saved-workspace-roots (avoids worktrees / stray paths).
  • Skips rows whose title starts with # Codex Task (auto-queued tasks, not user chats).
  • Skips rows whose rollout file is already within the cutoff (no-op for visible chats).
  • Spreads new mtimes over the last hour to preserve relative chronological order between touched chats.

In my run on Windows: 48 files touched, 0 SHA256 mismatches, sidebar populated correctly on next Codex Desktop launch.

huajiexiewenfeng · 1 month ago

For anyone landing here with the variant where the local session files still exist but Desktop no longer surfaces the old chats, I documented an unofficial recovery workflow here:

https://github.com/huajiexiewenfeng/codex-session-recovery

It does not replace an upstream fix. It dry-runs first, checks the local session index, global project/sidebar state, SQLite threads.cwd, exact cwd matching, and Windows path mismatches, then backs up before writing and verifies the recovery.

It may be useful only for cases where the local session data is still present and the issue is metadata/index/project association rather than truly missing transcript files.

Karube-Cyber · 1 month ago

I can add another macOS data point that looks related, with identifying details intentionally omitted.

Symptom

On Codex Desktop for macOS, adding one specific local project root caused every local chat pane to render blank, including chats from unrelated projects. Removing that one project root restored normal behavior for the other projects.

This looked like data loss in the UI, but the underlying local data was still readable from ~/.codex.

What made this case stand out

The affected project had a much larger local Codex history footprint than the other projects:

  • 48 local threads
  • ~164.7 MB of session/rollout JSONL
  • ~60,460 JSONL lines
  • ~78,000 total preview characters
  • >1.1B recorded tokens_used

Other project roots with smaller histories could be restored and opened normally.

Things we tried

These did not make the original project root safe to re-add:

  • PRAGMA quick_check on state_5.sqlite was OK.
  • Restored only project-list global-state keys, avoiding active/current/selected/open/last keys.
  • Archived the affected project threads.
  • Moved affected SQLite threads.cwd values to a quarantine workspace.
  • Synced process_manager/chat_processes.json.cwd with SQLite threads.cwd.
  • Removed matching affected project entries from chat_processes.json.
  • Moved the identified affected project JSONL files out of live sessions / archived_sessions.

Even after that, adding the original project root still made the Desktop UI blank.

Current workaround

The only stable workaround was:

  1. Keep the original project root out of Codex Desktop.
  2. Create a fresh clone at a new neutral path.
  3. Add that new path as the Codex project.
  4. Use exported/sanitized Markdown summaries for old chat context instead of reopening the old project history in Desktop.

Hypothesis

Codex Desktop may be deriving project/history membership from more than one local state source, not only state_5.sqlite or chat_processes.json. Stale workspace references in session metadata, combined with a very large project history, may be enough to break project enumeration or renderer state for all chats.

It would help a lot to have an official repair/reindex/quarantine command, and a UI error state instead of a blank pane when one project’s local history is too large or internally inconsistent.

I’m not attaching raw local files because they include private chat content, local paths, business context, and authentication-adjacent metadata.

wangyaok1 · 1 month ago

Hi everyone, I'm tracking the same Codex Desktop missing-thread/sidebar-history issue on macOS.

For those affected here: did you eventually get all hidden project threads back in Desktop? If yes, what exact method worked durably for you?

Examples I'm trying to distinguish:

  • official update fixed it
  • restart/reindex fixed it
  • codex resume --all <thread-id> rehydrated it
  • repairing session_index.jsonl / state_5.sqlite / .codex-global-state.json fixed it
  • Codex Wake or another local recovery helper fixed it
  • app bundle / listAllThreads patch fixed it
  • nothing worked yet

In my case, the missing project threads still exist in ~/.codex/sessions and state_5.sqlite, but Desktop does not hydrate them back into the project sidebar after restart. Any confirmed durable fix would help. Please do not share private session contents; sanitized method/results are enough.

Karube-Cyber · 1 month ago

In my macOS case, I did not get all hidden project threads durably restored back into the original Desktop project sidebar.

The stable workaround is still the one I described earlier:

  1. Keep the original project root out of Codex Desktop.
  2. Create a fresh clone at a new neutral path.
  3. Add that new path as the Codex project.
  4. Use exported/sanitized Markdown summaries for old chat context instead of reopening the old project history in Desktop.

For my case, restarting/reindexing, SQLite integrity checks, moving/quarantining cwd values, archiving affected threads, and removing related live session files did not make the original project root safe to re-add.

So my current result is: no durable full sidebar restoration yet; fresh clone at a new path is the only stable workaround I’ve confirmed.

wangyaok1 · 1 month ago

Adding a macOS datapoint after updating to the latest Desktop build.

Environment:

  • macOS 15.5 (24F74)
  • Apple Silicon / arm64
  • Codex Desktop: 26.608.12217
  • CFBundleVersion: 3722
  • Local storage: default ~/.codex

Recovery status on my affected project:

  • The update appears to have restored some previously missing threads.
  • It did not restore everything.
  • Older project threads are still missing from the Desktop sidebar/project history.
  • The local data still appears to exist. For the affected project, state_5.sqlite still has 251 active/non-archived rows with the expected project cwd.
  • Earlier checks also confirmed matching rollout JSONL/session files under ~/.codex/sessions.

So my current result is: 26.608.12217 looks like a partial recovery, not a full recovery. It would still be very helpful to have an official full local reindex/recovery command that rebuilds sidebar/project history from durable local state.