~/.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 

CLI और किसी session picker के बीच का पूरा contract बस इतना ही है। Claude Code और Codex दोनों अपने सेशन आपके home फ़ोल्डर में साधारण फ़ाइलों के रूप में लिखते हैं, और दोनों किसी सेशन को resume करने के लिए एक id लेते हैं। osFoundry के डेस्कटॉप ऐप की External agents स्क्रीन इन्हीं फ़ाइलों के ऊपर बना एक reader है (Rust, runtime/session_inventory.rs और runtime/external_sessions.rs)। यह बारह ऐप्स के सेशन दिखाती है, सर्च को title, फ़ोल्डर, branch, ऐप और id से मिलाती है, और terminal में सही resume कमांड चला देती है।

नीचे उसी का Claude Code और Codex वाला हिस्सा है, जिसे 150 लाइन की Python स्क्रिप्ट के रूप में दोबारा बनाया गया और उस Mac के असली session फ़ोल्डरों पर चलाया गया जिस पर यह पोस्ट लिखी गई। इस पोस्ट में कहीं भी transcript का कंटेंट प्रिंट नहीं किया गया है। हर संख्या या तो गिनती है, या साइज़, या समय।

डिस्क पर एक Claude Code सेशन कैसा दिखता है?

हर सेशन के लिए एक JSONL फ़ाइल, जिसका नाम session id होता है, और वह working directory के नाम वाले फ़ोल्डर के अंदर रहती है। फ़ोल्डर का नाम absolute path होता है, जिसमें हर वह कैरेक्टर जो अक्षर या अंक नहीं है, - से बदल दिया जाता है:

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

यहाँ के 40 transcripts पर जाँचा गया: 40 में से 40 फ़ोल्डर इस नियम से मेल खाते हैं। यह नियम lossy है (my.app और my-app टकरा जाते हैं, वैसे ही / और . भी), इसलिए फ़ोल्डर के नाम को कभी decode न करें। इसकी जगह records से cwd पढ़ें।

Sub-agents (Task tool वाले Explore वगैरह) अपने अलग transcripts <session id>/subagents/agent-<id>.jsonl में लिखते हैं। यहाँ ऐसे 129 थे। इन्हें कोई सेशन की तरह resume नहीं करता, और osFoundry ऐसी हर फ़ाइल को छोड़ देता है जिसका stem agent- से शुरू होता है।

हर लाइन एक record है जिसमें एक type होता है। सभी 40 फ़ाइलों की पहली 400 लाइनों में types गिनने पर:

type records index के लिए ज़रूरी keys
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+ ज़रूरत नहीं

बातचीत वाले records (user, assistant, attachment, system) सभी में sessionId, cwd, gitBranch, timestamp और version होते हैं। message.content या तो एक string होता है या blocks की list; इन लाइनों में block types थे tool_result 2,261, tool_use 2,223, thinking 1,224 और text 754। Title या तो custom-title record से आता है या, जो builds इसे लिखते हैं उनमें, aiTitle वाले ai-title record से। यहाँ हर फ़ाइल में custom-title था और किसी में ai-title नहीं, इसलिए दोनों पढ़ें।

हर फ़ाइल को पूरा parse क्यों न कर लें?

क्योंकि वे बड़ी हैं। यहाँ के 40 top-level transcripts कुल मिलाकर 19.4 GB के हैं। Median फ़ाइल 168 MB की है, सबसे बड़ी 2.07 GB की, और 90th percentile 1.94 GB।

Median फ़ाइल को लाइन-दर-लाइन json.loads से stream करने में 0.61 s लगे (11,558 लाइनें, Apple Silicon Mac पर करीब 279 MB/s, warm page cache के साथ)। इस रफ़्तार से सभी 40 का पूरा parse करीब 70 s लेगा, और तब तक आप कुछ दिखा भी नहीं पाएंगे।

List के लिए ज़रूरी जानकारी फ़ाइल के दोनों सिरों पर होती है। सभी 40 पर मापा गया:

  • cwd वाला पहला record ज़्यादा से ज़्यादा लाइन 2 पर था (median 2)
  • पहला gitBranch और पहला user record: ज़्यादा से ज़्यादा लाइन 2
  • 40 में से 40 फ़ाइलों में आख़िरी 512 KiB के अंदर एक title record था
  • पहली 80 लाइनों में से कोई भी 256 KiB से बड़ी नहीं थी

इसलिए index पहली 80 लाइनें और आख़िरी 512 KiB पढ़ता है। osFoundry भी यही करता है: session_inventory.rs में HEAD_LINES = 80 और हर लाइन पर 256 KiB की सीमा, और external_sessions.rs में SETTINGS_TAIL = 512 * 1024, जिसे latest model, effort, approval mode और branch के लिए पढ़ा जाता है। सभी 40 फ़ाइलों का head और tail पढ़ने में 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 

यहाँ capped readline मायने रखता है। Paste की गई इमेज वाला एक user turn कई मेगाबाइट की एक ही लाइन होता है, और सीधा-सादा for line in f उसे पूरा memory में खींच लेगा, सिर्फ़ फेंक देने के लिए।

Picker को असल में कौन-से fields चाहिए?

पाँच, और एक समय: ऐप, id, फ़ोल्डर, branch, title। Head से cwd, पहला gitBranch, और पहले असली user message से एक fallback title मिलता है (जो sidechain न हो, और < से शुरू न हो, क्योंकि command और hook wrappers ऐसे ही शुरू होते हैं)। Tail इन्हें सबसे नए title और सबसे नए branch से override कर देता है, क्योंकि लंबा सेशन अक्सर उस branch से ज़्यादा चलता है जिस पर वह शुरू हुआ था।

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} 

records() हर लाइन पर json.loads है, जिसमें खराब लाइनें छोड़ दी जाती हैं (tail read की पहली लाइन बीच से कटी होती है)। first_text() string content लौटाता है, या पहला text block। Last-used time फ़ाइल का mtime है, और osFoundry भी sorting के लिए यही इस्तेमाल करता है।

Codex यही जानकारी कहाँ रखता है?

Codex हर सेशन के लिए एक rollout तारीख़ वाले फ़ोल्डर ट्री में लिखता है, ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<thread id>.jsonl, और archived सेशन ~/.codex/archived_sessions/ में रखता है। यहाँ: 10 rollouts (28.5 MB) और 2 archived। हर rollout की पहली लाइन एक session_meta record होती है, जिसके payload में id, cwd, originator, source, cli_version, model_provider, timestamp और कुछ builds में git.branch होता है। बाकी event_msg, response_item (payload types message, function_call, function_call_output, reasoning, custom_tool_call, … के साथ), turn_context और compacted records होते हैं।

Codex एक index भी रखता है, इसलिए आपको rollouts खोलने की ज़रूरत ही नहीं पड़ती। ~/.codex/state_<n>.sqlite में एक threads table है; यहाँ वह state_5.sqlite है, 43 columns और 12 rows के साथ, जिनमें id, rollout_path, cwd, git_branch, title, model, reasoning_effort, approval_mode, sandbox_policy, source, archived और updated_at_ms शामिल हैं। एक से ज़्यादा state_<n> फ़ाइलें हो सकती हैं; osFoundry में codex_state_db सबसे बड़ा n लेता है, और स्क्रिप्ट भी यही करती है। जिन rows का source एक JSON object है, वे sub-agent threads हैं; उन्हें छोड़ दें।

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} 

एक चीज़ में दोबारा कोशिश करनी पड़ी: database को सिर्फ़ ?mode=ro से खोलने पर यहाँ unable to open database file एरर आया। immutable=1 जोड़ने से काम बन गया। Immutable SQLite को बताता है कि फ़ाइल खुली रहने के दौरान नहीं बदलेगी, इसलिए वह lock और -shm फ़ाइलों को छोड़ देता है; snapshot index के लिए यह ठीक है, और इसका मतलब है कि आप वह database भी पढ़ सकते हैं जिसे ऐप खोले हुए है। osFoundry, Codex का index SQLITE_OPEN_READ_ONLY से खोलता है, और जिन databases को कोई दूसरा ऐप लिखने के लिए खोले रख सकता है, उनके लिए immutable=1 वाला open इस्तेमाल करता है।

असली फ़ोल्डरों पर यह कितना तेज़ है?

$ 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 

52 सेशन (40 Claude Code, 12 Codex), हर एक के साथ फ़ोल्डर और title, और Claude Code वाले सभी 40 के साथ branch भी। पहला run सबसे cold है; बाकी warm थे। Index फ़ाइल 11,480 bytes की है। 70 s के पूरे parse के मुकाबले, 80 से 150 ms का मतलब है एक list जो तुरंत खुल जाए, न कि spinner वाली।

इसके बाद सर्च बस पाँच fields पर एक substring टेस्ट है, वही fields जो osFoundry का matchesQuery 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) 

उसी ढाँचे में बनाए गए एक नकली home पर (उदाहरण डेटा, असली सेशन नहीं):

$ 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 

Message text के अंदर सर्च करने का मतलब होगा 19.4 GB पढ़ना, या उस सब का full-text index बनाना जो आपने कभी किसी agent में paste किया हो। फ़ोल्डर और branch पर सर्च करने से सेशन प्रोजेक्ट के नाम से मिल जाता है, और वैसे भी आपको यही याद रहता है।

सुरक्षित resume में और क्या जुड़ता है?

claude --resume <id> प्रिंट करना आसान हिस्सा है। osFoundry इसके आसपास जो जोड़ता है (external_sessions.rs में plan_command और launch plan, साथ में terminal खोलने से पहले स्क्रीन के checks), वह कॉपी करने लायक है:

  • रिकॉर्ड किए गए फ़ोल्डर में चलाएँ, और अगर वह अब मौजूद नहीं है तो कहीं और resume करने की बजाय मना कर दें।
  • रिकॉर्ड किए गए branch की तुलना git -C <cwd> rev-parse --abbrev-ref HEAD से करें। अलग हों तो बताएँ और पूछें; कभी checkout न करें।
  • Model (--model, Codex में -m) और effort (--effort, Codex में -c model_reasoning_effort="…") साथ ले जाएँ।
  • रिकॉर्ड किया गया approval mode तभी साथ ले जाएँ जब वह CLI के default जितना या उससे ज़्यादा strict हो। Claude Code के लिए यह default या plan है; Codex के लिए approval untrusted या on-request, sandbox read-only या workspace-write। इससे ढीली कोई भी चीज़ रोक ली जाती है, जब तक व्यक्ति उसी एक launch के लिए उसे खुद चालू न करे।

पूरी sessidx.py ऊपर के तीन हिस्से हैं, साथ में करीब 30 लाइन का glue code (home फ़ोल्डरों के लिए env overrides, JSON output, और find व resume subcommands)। यहाँ इस्तेमाल किए गए Mac पर claude या codex की कोई binary नहीं है, इसलिए resume लाइन सिर्फ़ प्रिंट होती है, चलाई नहीं जाती।

अगर आप इसे खुद जोड़ रहे हैं

  • फ़ाइलों को ही API मानें: सिर्फ़ पढ़ें, उनके पास कभी कुछ न लिखें, और account का नाम निकालने के लिए CLI की auth फ़ाइलें कभी न खोलें।
  • पूरे transcripts नहीं, सिर्फ़ head और tail पढ़ें। लाइन की लंबाई पर सीमा रखें ताकि paste की गई एक इमेज आपकी memory में अपने पूरे साइज़ जितनी जगह न ले।
  • cwd records से लें, escape किए गए फ़ोल्डर नाम से नहीं।
  • तारीख़ वाले ट्री में घूमने की बजाय Codex की state_<n>.sqlite (सबसे बड़ा n) को प्राथमिकता दें, और अगर ऐप उसे खोले रख सकता है तो उसे immutable मोड में खोलें।
  • List में sub-agent transcripts (agent-*.jsonl, JSON source वाले threads) न दिखाएँ; उन्हें कोई सीधे resume नहीं करता।
  • पहले से तय कर लें कि resume क्या-क्या बदल सकता है। एक सुरक्षित default: फ़ोल्डर हाँ, model हाँ, ढीले approvals सिर्फ़ माँगने पर, branch कभी नहीं।

osFoundry के अपने agents का Sessions view एक अलग source से काम करता है; उसका विवरण osFoundry agent sessions पेज पर है।