sess

Find and read a session from either host
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 shutil, tempfile

Claude Code sessions and Codex threads are stored differently and read by different modules, but a person reaching for one has only an id and a question. find_sess takes that id, from either host, and says which host owns it and where the transcript is; sess_dlg reads it into a dialog for the aidialog tools. Ids may be given as any unique prefix.

Where ant.sess2dlg and oai.thread2dlg are faithful conversions, sess_dlg is a reading view: it drops the host’s bookkeeping and the harness’s injected turns, and it reaches back through compactions by default, so the dialog holds the conversation rather than the machinery around it. The sess2nb command line writes that view to an ipynb.

Finding a session


source

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 fixtures the ant and oai notebooks use are ordinary transcripts, so putting one where each host keeps its sessions is enough to find it. A Claude transcript is named by its session id, and eight characters of that id are plenty:

proj = Path(tempfile.mkdtemp())
sd = ant.sess_dir(proj)
sd.mkdir(parents=True)
shutil.copy(Path(files('llmsurgery')/'data'/'ant'/'source.jsonl'), sd/'ab12cd34-0000-4000-8000-000000000000.jsonl')
find_sess('ab12cd34', proj)

A Codex rollout is found the same way, from a thread id embedded in a longer filename, so find_sess also reports which host owns the id it was given:

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)

Reading a session

Both hosts record a transcript in append order, so reading the whole history needs no chain walking: for Claude, ant.conv_recs keeps the conversation records in the order they happened, and for Codex, oai.response_items returns every recorded item, superseded history included. since_compact swaps each for its narrower counterpart, ant.sess_thread and oai.active_items, giving only what the model would see now.

Filtering is what makes this a reading view rather than a transcript. Compaction summaries go, because with the full history present they restate what is already there. Turns the harness injected go too: a skill body arrives as a user turn indistinguishable from a typed one except by its opening line. What remains is what a person said and what the assistant said back.


source

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


source

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


source

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()

The dialog is self-describing: nb-level meta records which transcript it came from and the conversation’s true time span (first and last record timestamps), and each message’s cell meta carries its source record’s created time and uid. File times can lie – opening an old session bumps its mtime – so provenance lives in the data:

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

Claude transcripts can contain the same record twice (chain restarts replay records into the append-order file). A replayed record is the same message, so the reading view keeps the first occurrence – duplicating every line of a transcript changes nothing:

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])

strip_tools leaves only what was said, which is the form to read when the question is what was decided rather than what was run:

bare = sess_dlg('ab12cd34', proj, strip_tools=True)
assert not any('{.tool}' in (m.ai_res or '') for m in bare.messages)
bare.summary()

The command line

sess2nb writes that dialog to an ipynb, so a session becomes a notebook to read, search, or paste from. It returns the path when called from Python and prints it when run as a command.


source

sess2nb

def sess2nb(
    ref:str, # Session id or unique id prefix
    Out:str=None, # Output path; `<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?
):

Write a Claude session or Codex thread to an ipynb dialog

with tempfile.TemporaryDirectory() as td:
    out = sess2nb('ab12cd34', f'{td}/sess.ipynb')
    saved = read_ipynb(out)
test_eq(len(saved.messages), len(sess_dlg('ab12cd34', proj).messages))
out.name

Cleanup

The Claude fixture has to sit in ~/.claude/projects to be findable, so remove it again along with both scratch homes.

shutil.rmtree(sd)
shutil.rmtree(proj)
shutil.rmtree(home)