DEV Community

Cover image for Cara osFoundry menemukan sesi Claude Code dan Codex kamu di disk, dibuat ulang dengan Python
BBT for BBT Indonesia

Posted on

Cara osFoundry menemukan sesi Claude Code dan Codex kamu di disk, dibuat ulang dengan Python

~/.claude/projects/-Users-alex-code-shop-api/c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58.jsonl
~/.claude/projects/-Users-alex-code-shop-api/c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58/subagents/agent-<id>.jsonl
~/.codex/state_5.sqlite                      -> table threads (id, rollout_path, cwd, git_branch, title, …)
~/.codex/sessions/2026/11/04/rollout-2026-11-04T10-00-00-<thread id>.jsonl

$ claude --resume c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58
$ codex resume 019a2f1c-7d0e-7b31-9c4e-5a1f0e2d8b63
Enter fullscreen mode Exit fullscreen mode

Hanya itu kontrak antara CLI dan sebuah session picker. Claude Code dan Codex masing-masing menyimpan sesinya sebagai file biasa di folder home kamu, dan masing-masing menerima sebuah id untuk melanjutkan satu sesi. Layar External agents di aplikasi desktop osFoundry adalah pembaca yang berjalan di atas file-file itu (Rust, runtime/session_inventory.rs dan runtime/external_sessions.rs). Layar ini menampilkan sesi dari dua belas aplikasi, mencocokkan pencarian dengan judul, folder, branch, aplikasi, dan id, lalu menjalankan perintah resume yang tepat di terminal.

Di bawah ini bagian Claude Code dan Codex dari fitur itu, dibuat ulang sebagai skrip Python 150 baris dan dijalankan pada folder sesi asli di Mac tempat tulisan ini dibuat. Tidak ada isi transkrip yang ditampilkan di mana pun dalam tulisan ini. Setiap angka adalah jumlah, ukuran, atau waktu.

Seperti apa satu sesi Claude Code di disk?

Satu file JSONL per sesi, dinamai dengan id sesi, di dalam folder yang dinamai sesuai direktori kerja. Nama foldernya adalah path absolut dengan setiap karakter yang bukan huruf atau angka diganti -:

re.sub(r"[^A-Za-z0-9]", "-", cwd)   # /Users/alex/code/shop-api -> -Users-alex-code-shop-api
Enter fullscreen mode Exit fullscreen mode

Dicek terhadap 40 transkrip di mesin ini: 40 dari 40 folder sesuai aturan itu. Aturannya lossy (my.app dan my-app bentrok, begitu juga / dan .), jadi jangan pernah men-decode nama folder. Baca cwd dari record-nya.

Sub-agent (Explore dan kawan-kawan dari tool Task) menulis transkripnya sendiri di <session id>/subagents/agent-<id>.jsonl. Di sini ada 129. Itu bukan sesi yang akan di-resume orang, dan osFoundry melewati setiap file yang stem-nya diawali agent-.

Setiap baris adalah satu record dengan sebuah type. Menghitung type di 400 baris pertama dari masing-masing 40 file:

type record key yang penting untuk index
assistant 4,060 requestedModel, effort, gitBranch, message.model
attachment 3,197 cwd, gitBranch
user 2,469 cwd, gitBranch, permissionMode, isSidechain, message.content, entrypoint
custom-title 922 customTitle
agent-name 827 agentName
system, mode, queue-operation, file-history-, api-request, … 2,000+ tidak diperlukan

Record percakapan (user, assistant, attachment, system) semuanya membawa sessionId, cwd, gitBranch, timestamp, dan version. message.content bisa berupa string atau list of blocks; di baris-baris itu tipe block-nya adalah tool_result 2,261, tool_use 2,223, thinking 1,224, dan text 754. Judul berasal dari record custom-title atau, pada build yang menulisnya, record ai-title dengan aiTitle. Di sini semua file punya custom-title dan tidak ada yang punya ai-title, jadi baca keduanya.

Kenapa tidak parse semua file saja?

Karena ukurannya besar. Ke-40 transkrip level atas di sini totalnya 19.4 GB. Median file-nya 168 MB, yang terbesar 2.07 GB, dan persentil ke-90 1.94 GB.

Men-stream file median lewat json.loads baris per baris butuh 0.61 s (11,558 baris, sekitar 279 MB/s di Mac Apple Silicon, dengan page cache yang sudah hangat). Dengan kecepatan itu, parse penuh untuk semua 40 file akan makan sekitar 70 s, bahkan sebelum ada yang ditampilkan.

Data yang dibutuhkan sebuah daftar ada di kedua ujung file. Diukur pada semua 40 file:

  • record pertama yang punya cwd paling lambat ada di baris 2 (median 2)
  • gitBranch pertama dan record user pertama: paling lambat baris 2
  • record judul ada di dalam 512 KiB terakhir pada 40 dari 40 file
  • tidak ada baris di 80 baris pertama yang lebih dari 256 KiB

Jadi index membaca 80 baris pertama dan 512 KiB terakhir. Itu juga yang dilakukan osFoundry: HEAD_LINES = 80 dengan batas 256 KiB per baris di session_inventory.rs, dan SETTINGS_TAIL = 512 * 1024 di external_sessions.rs, yang dibaca untuk model, effort, mode approval, dan branch terbaru. Membaca head dan tail dari semua 40 file butuh 86 ms.

HEAD_LINES, LINE_CAP, TAIL_BYTES = 80, 256 * 1024, 512 * 1024


def head(p, n=HEAD_LINES):
    out = []
    with open(p, "rb") as f:
        for _ in range(n):
            line = f.readline(LINE_CAP + 1)
            if not line:
                break
            if len(line) > LINE_CAP:          # a pasted file or image: skip the rest of it
                while line and not line.endswith(b"\n"):
                    line = f.readline(1 << 20)
                continue
            out.append(line)
    return out


def tail(p, nbytes=TAIL_BYTES):
    with open(p, "rb") as f:
        f.seek(0, 2)
        size = f.tell()
        f.seek(max(0, size - nbytes))
        lines = f.read().split(b"\n")
    return lines[1:] if size > nbytes else lines   # first piece is a cut line
Enter fullscreen mode Exit fullscreen mode

readline yang dibatasi itu penting. Satu giliran user yang berisi gambar yang di-paste adalah satu baris berukuran beberapa megabyte, dan for line in f biasa akan menarik semuanya ke memori hanya untuk dibuang.

Field apa saja yang benar-benar dibutuhkan picker?

Lima, ditambah waktu: aplikasi, id, folder, branch, judul. Head memberikan cwd, gitBranch pertama, dan judul cadangan dari pesan user asli yang pertama (bukan sidechain, dan bukan yang diawali <, karena begitulah wrapper command dan hook dimulai). Tail menimpanya dengan judul dan branch terbaru, karena sesi yang panjang sering bertahan lebih lama dari branch tempat ia dimulai.

def claude_sessions():
    for proj in sorted((CLAUDE / "projects").glob("*/")):
        for p in proj.glob("*.jsonl"):
            if p.stem.startswith("agent-"):           # sub-agent transcript, not resumable
                continue
            st = p.stat()
            cwd = branch = title = None
            for v in records(head(p)):
                cwd = cwd or v.get("cwd")
                branch = branch or (v.get("gitBranch") or None)
                if title is None and v.get("type") == "user" and not v.get("isSidechain"):
                    t = (first_text((v.get("message") or {}).get("content")) or "").strip()
                    if t and not t.startswith("<"):
                        title = t[:80]
                if cwd and branch and title:
                    break
            for v in records(tail(p)):                 # newest wins: titles, branch moves
                for k in ("customTitle", "aiTitle"):
                    if v.get(k):
                        title = v[k][:100]
                branch = v.get("gitBranch") or branch
            yield {"app": "claude-code", "id": p.stem, "cwd": cwd, "branch": branch,
                   "title": title, "mtime": int(st.st_mtime * 1000), "bytes": st.st_size}
Enter fullscreen mode Exit fullscreen mode

records() adalah json.loads per baris dengan baris rusak dilewati (baris pertama dari pembacaan tail terpotong di tengah). first_text() mengembalikan isi yang berupa string, atau block text pertama. Waktu terakhir dipakai diambil dari mtime file, yang juga dipakai osFoundry untuk pengurutan.

Di mana Codex menyimpan data yang sama?

Codex menulis satu rollout per sesi di bawah pohon folder berdasarkan tanggal, ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<thread id>.jsonl, dan menyimpan yang diarsipkan di ~/.codex/archived_sessions/. Di sini: 10 rollout (28.5 MB) dan 2 yang diarsipkan. Baris pertama setiap rollout adalah record session_meta yang payload-nya berisi id, cwd, originator, source, cli_version, model_provider, timestamp dan, di beberapa build, git.branch. Sisanya adalah record event_msg, response_item (dengan tipe payload message, function_call, function_call_output, reasoning, custom_tool_call, …), turn_context, dan compacted.

Codex juga menyimpan index, jadi kamu sama sekali tidak perlu membuka rollout. ~/.codex/state_<n>.sqlite punya tabel threads; di sini file-nya state_5.sqlite dengan 43 kolom dan 12 baris, termasuk id, rollout_path, cwd, git_branch, title, model, reasoning_effort, approval_mode, sandbox_policy, source, archived, dan updated_at_ms. Bisa ada lebih dari satu file state_<n>; codex_state_db di osFoundry mengambil n tertinggi, dan skripnya juga begitu. Baris yang source-nya berupa objek JSON adalah thread sub-agent; lewati saja.

def codex_sessions():
    db = codex_db()
    if db:
        con = sqlite3.connect(f"file:{db}?mode=ro&immutable=1", uri=True)
        q = ("SELECT id, cwd, git_branch, title, COALESCE(updated_at_ms, updated_at*1000), source "
             "FROM threads ORDER BY 5 DESC LIMIT 300")
        for id_, cwd, br, title, ms, source in con.execute(q):
            if (source or "").startswith("{"):        # sub-agent thread
                continue
            yield {"app": "codex", "id": id_, "cwd": cwd, "branch": br,
                   "title": (title or "")[:100] or None, "mtime": ms or 0, "bytes": None}
        return
    for p in (CODEX / "sessions").glob("*/*/*/rollout-*.jsonl"):
        v = next(records(head(p, 1)), {})
        if v.get("type") != "session_meta":
            continue
        pl = v.get("payload", {})
        yield {"app": "codex", "id": pl.get("id"), "cwd": pl.get("cwd"),
               "branch": (pl.get("git") or {}).get("branch"), "title": None,
               "mtime": int(p.stat().st_mtime * 1000), "bytes": p.stat().st_size}
Enter fullscreen mode Exit fullscreen mode

Satu hal yang butuh percobaan kedua: membuka database hanya dengan ?mode=ro gagal di sini dengan unable to open database file. Menambahkan immutable=1 berhasil. Immutable memberi tahu SQLite bahwa file tidak akan berubah selama dibuka, jadi SQLite melewati lock dan file -shm; itu aman untuk index berupa snapshot, dan artinya kamu bisa membaca database yang sedang dibuka oleh aplikasinya. osFoundry membuka index Codex dengan SQLITE_OPEN_READ_ONLY, dan memakai open dengan immutable=1 untuk database yang mungkin sedang dibuka aplikasi lain untuk menulis.

Seberapa cepat di folder sungguhan?

$ python3 sessidx.py build          # four runs, same Mac, real ~/.claude and ~/.codex
52 sessions  claude 154 ms  codex 3 ms
52 sessions  claude 80 ms  codex 2 ms
52 sessions  claude 83 ms  codex 2 ms
52 sessions  claude 107 ms  codex 4 ms
Enter fullscreen mode Exit fullscreen mode

52 sesi (40 Claude Code, 12 Codex), semuanya punya folder dan judul, dan ke-40 sesi Claude Code punya branch. Run pertama adalah yang paling dingin; sisanya sudah hangat. File index-nya 11,480 byte. Dibandingkan parse penuh 70 s, 80 sampai 150 ms adalah beda antara daftar yang langsung terbuka dan daftar yang menampilkan spinner.

Pencarian lalu cukup berupa tes substring pada lima field, sama dengan yang dipakai matchesQuery milik osFoundry di lib/external-sessions.ts:

def matches(r, q):
    q = q.strip().lower()
    return not q or any(q in str(v).lower() for v in (r["title"], r["cwd"], r["branch"], r["app"], r["id"]) if v)
Enter fullscreen mode Exit fullscreen mode

Pada home palsu yang dibuat dengan struktur yang sama (data contoh, bukan sesi asli):

$ SESSIDX_HOME=./fx python3 sessidx.py find checkout
codex       019a2f1c  test/checkout-e2e    -
claude-code c5e1a7b2  fix/checkout-retry   Fix double charge on checkout retry
$ SESSIDX_HOME=./fx python3 sessidx.py resume c5e1
cd /Users/alex/code/shop-api && claude --resume c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58
Enter fullscreen mode Exit fullscreen mode

Mencari di dalam teks pesan berarti membaca 19.4 GB itu, atau membangun full-text index dari semua yang pernah kamu paste ke agent. Mencari berdasarkan folder dan branch menemukan sesi lewat nama proyek, dan memang itu yang biasanya kamu ingat.

Apa yang ditambahkan oleh resume yang aman?

Mencetak claude --resume <id> itu bagian yang gampang. Yang ditambahkan osFoundry di sekitarnya (plan_command dan launch plan di external_sessions.rs, plus pengecekan yang dilakukan layar sebelum membuka terminal) layak ditiru:

  • Jalankan di folder yang tercatat, dan tolak kalau folder itu sudah tidak ada, alih-alih melanjutkan di tempat lain.
  • Bandingkan branch yang tercatat dengan git -C <cwd> rev-parse --abbrev-ref HEAD. Kalau berbeda, beri tahu dan tanyakan; jangan pernah checkout.
  • Bawa model (--model, di Codex -m) dan effort (--effort, di Codex -c model_reasoning_effort="…").
  • Bawa mode approval yang tercatat hanya kalau setidaknya seketat default CLI. Untuk Claude Code itu default atau plan; untuk Codex, approval untrusted atau on-request, sandbox read-only atau workspace-write. Yang lebih longgar ditahan, kecuali orangnya memilih untuk mengaktifkannya pada peluncuran itu saja.

sessidx.py lengkapnya terdiri dari tiga bagian di atas plus sekitar 30 baris kode perekat (override env untuk folder home, output JSON, subcommand find dan resume). Tidak ada binary claude atau codex di Mac yang dipakai di sini, jadi baris resume-nya dicetak, bukan dijalankan.

Kalau kamu mau membuatnya sendiri

  • Perlakukan file-file itu sebagai API: hanya baca, jangan pernah menulis di sebelahnya, dan jangan pernah membuka file auth milik CLI untuk mengambil nama akun.
  • Baca head dan tail, bukan seluruh transkrip. Batasi panjang baris supaya satu gambar yang di-paste tidak memakan memori sebesar ukurannya.
  • Ambil cwd dari record, bukan dari nama folder yang di-escape.
  • Utamakan state_<n>.sqlite milik Codex (n tertinggi) daripada menelusuri pohon tanggal, dan buka dalam mode immutable kalau aplikasinya mungkin sedang memegangnya.
  • Jangan masukkan transkrip sub-agent (agent-*.jsonl, thread dengan source JSON) ke daftar; tidak ada yang me-resume-nya secara langsung.
  • Tentukan di awal apa saja yang boleh diubah oleh resume. Default yang aman: folder ya, model ya, approval yang lebih longgar hanya kalau diminta, branch tidak pernah.

Tampilan Sessions untuk agent milik osFoundry sendiri bekerja dari sumber yang berbeda; penjelasannya ada di halaman sesi agent osFoundry.

Top comments (0)