from fastcore.test import *
from importlib.resources import files
from aidialog.dialog import Dialog
from aidialog.dlgskill import summary_dlg
from aidialog.ipynb import read_ipynb
import json, shutil, tempfilesess
Have a session id but don’t know which host it belongs to? find_sess searches Claude Code and Codex transcripts. It returns the host and path. A unique id prefix works too. Without a ref, it tries the current Codex thread id, then Claude’s current session. No match raises FileNotFoundError; a match on both hosts raises ValueError.
sess_dlg opens the result as an aidialog dialog; the sess2nb command saves that dialog to a notebook. Use these functions to read a conversation, not to reproduce the host’s complete transcript. They remove bookkeeping and recognize some injected user turns, such as skill instructions. By default they include recorded conversation from before compaction. ant.sess2dlg and oai.thread2dlg retain more host context for editing.
Finding a session
find_sess
def find_sess(
ref:NoneType=None, # Session id or unique id prefix; the current session if None
cwd:NoneType=None, # Project directory, for Claude sessions
codex_home:NoneType=None, # Codex home; `oai.CODEX_HOME` if None
):The host owning session ref and its transcript path: ('ant'|'oai', path)
The checked-in fixtures use each host’s transcript format. Copying them into the expected folders lets us test lookup without creating a live session. Claude filenames use the session id. This eight-character prefix uniquely identifies our fixture.
ant.sess_dir normally points beneath ~/.claude/projects. Isolate that session storage before running these fixture examples:
proj = Path(tempfile.mkdtemp())
sd = ant.sess_dir(proj)
sd.mkdir(parents=True)
shutil.copy(ant.ant_data/'source.jsonl', sd/'ab12cd34-0000-4000-8000-000000000000.jsonl')
find_sess('ab12cd34', proj)('ant',
Path('/private/tmp/llmsurgery-rewrite-Ya2CZR/test-state/sess-isolated-q3o0w_31/claude-projects/-private-tmp-llmsurgery-rewrite-Ya2CZR-test-state-sess-isolated-q3o0w-31-tmplymm3o-v/ab12cd34-0000-4000-8000-000000000000.jsonl'))
Codex embeds the thread id in a longer filename. Its fixture lives in a temporary Codex home. The returned host tells us which reader to use:
home = Path(tempfile.mkdtemp())
ses = home/'sessions'/'2026'/'01'
ses.mkdir(parents=True)
shutil.copy(Path(files('llmsurgery')/'data'/'oai'/'source.jsonl'), ses/'rollout-2026-01-24T02-44-30-ef56ab78.jsonl')
find_sess('ef56ab78', codex_home=home)('oai',
Path('/private/tmp/llmsurgery-rewrite-Ya2CZR/test-state/sess-isolated-q3o0w_31/tmp3hr2bf4v/sessions/2026/01/rollout-2026-01-24T02-44-30-ef56ab78.jsonl'))
For “the session I just finished here”, use recent_sess. It compares modification times across both hosts and returns the newest transcript for the project directory. Claude stores these files in the project’s transcript folder; Codex lookup uses project_thread. No match raises FileNotFoundError.
recent_sess
def recent_sess(
cwd:NoneType=None, # Project directory; the current directory if None
codex_home:NoneType=None, # Codex home; `oai.CODEX_HOME` if None
):The host with the newest transcript for cwd, and its path: ('ant'|'oai', path)
rp = ses/'rollout-2026-01-25T09-00-00-aabbccdd.jsonl'
rp.write_text(json.dumps(dict(type='session_meta',payload=dict(id='aabbccdd',cwd=str(proj))))+'\n')
ap = sd/'ab12cd34-0000-4000-8000-000000000000.jsonl'
mt = ap.stat().st_mtime+1
os.utime(rp,(mt,mt))
test_eq(recent_sess(proj,home), ('oai',rp))
os.utime(ap,(mt+1,mt+1))
test_eq(recent_sess(proj,home), ('ant',ap))
recent_sess(proj,home)('ant',
Path('/private/tmp/llmsurgery-rewrite-Ya2CZR/test-state/sess-isolated-q3o0w_31/claude-projects/-private-tmp-llmsurgery-rewrite-Ya2CZR-test-state-sess-isolated-q3o0w-31-tmplymm3o-v/ab12cd34-0000-4000-8000-000000000000.jsonl'))
Reading a session
Both hosts append conversation records in time order. To read the full recorded conversation, we use ant.conv_recs for Claude and oai.response_items for Codex. This includes history that compaction later superseded.
Set since_compact=True to start from Claude’s current message chain or Codex’s active replacement history. The reading filters still apply, so this is not an exact copy of the model’s input.
We drop Claude compaction summaries rather than repeat the earlier conversation. We also filter user turns whose opening text matches known injected content: skills, hook feedback, environment context, and similar instructions. A separate pattern recognizes slash commands with at most one argument. These are text heuristics, not a reliable distinction between everything a person wrote and everything a host inserted.
sess_chat returns canonical messages, oldest first. Claude records can repeat after a chain restart, so it keeps the first occurrence of each UUID and removes compaction summaries. Codex conversion uses all response items by default, or active history with since_compact=True:
sess_chat
def sess_chat(
host, # `'ant'` for a Claude session, `'oai'` for a Codex thread
path, # The transcript path, e.g. from `find_sess`
since_compact:bool=False, # Only the conversation since the last compaction?
):Canonical messages for the conversation recorded in path, oldest first
path_dlg filters injected turns and builds a dialog from a transcript path. sess_dlg first resolves an id. Both limit each rendered tool string to 10 characters by default; pass mx=None for the full strings.
sess_dlg
def sess_dlg(
ref:NoneType=None, # Session id or unique id prefix; the current session if None
cwd:NoneType=None, # Project directory, for Claude sessions
codex_home:NoneType=None, # Codex home; `oai.CODEX_HOME` if None
name:NoneType=None, # Dialog name; the transcript's id if None
mx:int=10, # Maximum characters per rendered tool input/output string; None disables truncation
since_compact:bool=False, # Only the conversation since the last compaction?
strip_tools:bool=False, # Drop tool calls and their results entirely?
):The conversation of a Claude session or Codex thread as a dialog, ready to read or save
path_dlg
def path_dlg(
host, # `'ant'` for a Claude session, `'oai'` for a Codex thread
path, # The transcript path, e.g. from `find_sess`
name:NoneType=None, # Dialog name; the transcript's stem if None
mx:int=10, # Maximum characters per rendered tool input/output string; None disables truncation
since_compact:bool=False, # Only the conversation since the last compaction?
strip_tools:bool=False, # Drop tool calls and their results entirely?
):The conversation of the transcript at path as a dialog, its nb meta recording source and time span
sess_dlg('ab12cd34', proj).summary()33f0ab78:p:Use the ant-fixture skill. Then use Bash to run `printf 'bash fixture\n'`. Then use clikernel to evaluate `6*7`. After all tools finish, reply exactly: fixture complete.
>[614] I'll execute these steps in sequence.¶```json {.tool}¶{¶ "id": "toolu_01AHDMAStWe9M8h1U7Mno6e8",¶ "name": "Skill",¶ "args": {¶ "skill": "ant-fixture"¶ },¶ "res…[443]
Notebook metadata records the host and source path under llmsurgery. When retained messages have timestamps, created and last give their earliest and latest times. These are not necessarily the first and last records in the original file.
Message metadata can also carry the source timestamp and UUID. The Claude fixture below has both. File times can lie—opening an old session can bump its mtime—so use the recorded timestamps when you need to date the conversation.
md = sess_dlg('ab12cd34', proj)
lm = md.meta['llmsurgery']
test_eq(lm['host'], 'ant')
assert lm['source'].endswith('.jsonl')
assert lm['created'] <= lm['last']
assert all(m.meta['created'] and m.meta['uid'] for m in md.messages)
lm{'host': 'ant',
'source': '/private/tmp/llmsurgery-rewrite-Ya2CZR/test-state/sess-isolated-q3o0w_31/claude-projects/-private-tmp-llmsurgery-rewrite-Ya2CZR-test-state-sess-isolated-q3o0w-31-tmplymm3o-v/ab12cd34-0000-4000-8000-000000000000.jsonl',
'created': '2026-07-17T02:51:28.115Z',
'last': '2026-07-17T02:51:40.442Z'}
A chain restart can replay Claude records into the same transcript. Duplicating this fixture’s lines should not duplicate its messages:
ap = sd/'ab12cd34-0000-4000-8000-000000000000.jsonl'
lines = ap.read_text().splitlines()
ap.write_text('\n'.join(lines+lines)+'\n')
d2 = sess_dlg('ab12cd34', proj)
test_eq([m.id for m in d2.messages], [m.id for m in md.messages])
test_eq([m.content for m in d2.messages], [m.content for m in md.messages])When you want to read what was decided rather than what was run, pass strip_tools=True. It removes tool-role messages and tool-call parts. Other content, including text and images, remains:
bare = sess_dlg('ab12cd34', proj, strip_tools=True)
assert not any('{.tool}' in (m.ai_res or '') for m in bare.messages)
bare.summary()017e46aa:p:Use the ant-fixture skill. Then use Bash to run `printf 'bash fixture\n'`. Then use clikernel to evaluate `6*7`. After all tools finish, reply exactly: fixture complete.
> I'll execute these steps in sequence.¶fixture complete
The command line
sess2nb writes a notebook you can read, search, or paste from. From Python it returns the output path. The command prints that path with the message count, host, and transcript name. Pass an output path with -o; the function adds .ipynb if needed.
Choose either a session id or -r for the newest session in the current directory. The usual reading options also apply: limit rendered tool strings with mx, keep only the post-compaction view with Since_compact, or remove tools with strip_Tools.
sess2nb
def sess2nb(
ref:str=None, # Session id or unique id prefix
Out:str=None, # Output path, `.ipynb` added if missing; `<id>.ipynb` in the current directory if None
mx:int=10, # Maximum characters per rendered tool input/output string
Since_compact:bool=False, # Only the conversation since the last compaction?
strip_Tools:bool=False, # Drop tool calls and their results entirely?
Recent:bool=False, # Convert the newest session for the current directory instead?
):Write a Claude session or Codex thread to an ipynb dialog
with tempfile.TemporaryDirectory() as td:
out = sess2nb('ab12cd34', f'{td}/sess')
saved = read_ipynb(out)
test_eq(len(saved.messages), len(sess_dlg('ab12cd34', proj).messages))
test_eq(out.name, 'sess.ipynb')
out.name'sess.ipynb'
-r is the CLI spelling of Recent=True. Here it selects the Claude fixture, whose modification time we made newest above. Passing both a ref and Recent, or neither, raises an error:
old = Path.cwd()
os.chdir(proj)
with tempfile.TemporaryDirectory() as td: n = len(read_ipynb(sess2nb(Recent=True, Out=f'{td}/r.ipynb')).messages)
os.chdir(old)
test_eq(n, len(sess_dlg('ab12cd34', proj).messages))
with expect_fail(contains='exactly one'): sess2nb()
with expect_fail(contains='exactly one'): sess2nb('ab12cd34', Recent=True)
n1
Cleanup
Remove the copied Claude fixture folder and the two temporary directories. Leave the hosts’ other sessions and the checked-in fixtures alone.
shutil.rmtree(sd)
shutil.rmtree(proj)
shutil.rmtree(home)