Docs / Guides / Find and resume the right conversation
Guides
Find and resume the right conversation
A long-running agent session builds up several conversations. How to list them, resume the one you want, and why an old 401 in the scrollback isn't what it looks like.
A session you drive for weeks doesn’t hold one conversation — it holds a stack of them, one per time you
started fresh. Claude Code
stores each as a transcript file under
~/.claude/projects/<project>/. --continue resumes the newest, which is usually what you want — until
it isn’t, and you’re staring at three transcripts with no idea which is the thread you care about.
Two things make this manageable: a way to see the conversations with enough context to tell them apart,
and knowing that a scary-looking 401 in a resumed session is often just old text being redrawn.
List a session’s conversations
amux-convos prints a session’s conversations newest-first, each with when it was last active, how many
messages it holds, its id, and a snippet of the last thing you said:
$ amux-convos mybot
1. 2026-07-16 18:20 2104 msgs 116f2ed4-8723-4ddf-af7c-d3bb3423ce1c
“so the replies should appear as a thread, not a new channel post”
2. 2026-07-06 16:47 1384 msgs 2eef05b1-7a25-4ee2-91cf-699fff7ece78
“we still have the bucket backup pending, right?”
3. 2026-07-03 08:22 6058 msgs eef3c73b-1bd5-4bdc-9eba-84491d91b9dd
“it's running on a server I pay for”
resume: amux-convos mybot --resume <N> (or: amux start mybot --resume <UUID>)
The snippet is what makes it usable — you recognize the conversation by what you were last talking about, not
by a hex id. Options: -n N (top N only), --less (page), --full (don’t truncate the snippet).
Resume a specific one
Three ways, from most to least convenient:
amux-convos mybot --resume 1 # restart the session on conversation #1 from the list
amux start mybot --resume 116f2ed4-… # by id, if you already know it
From inside a running session, the agent’s own /resume command opens a picker too. Plain
amux start mybot --continue always takes the newest — fine once the one you want is the newest again.
The 401 in the scrollback may be history
Here’s the trap that costs an afternoon if you don’t know it. You resume a session and the pane shows:
❯ so the replies should appear as a thread, not a new channel post
⏺ Please run /login · API Error: 401 OAuth access token has expired.
It looks dead. It usually isn’t. When a session resumes, the agent redraws its saved transcript into the
pane — and if auth was genuinely broken at some earlier point, those 401 / Please run /login lines were
written into the transcript back then. Resuming replays them. They are history, not the current state.
Judge auth by a fresh reply, not the scrollback
401s will still answer a brand-new prompt. Don’t conclude “auth is down” from the
pane, and don’t wipe or reset the session over it — the transcript, old errors and all, is intact and
harmless.❯ reply with exactly READY
⏺ READY ← a live answer: auth is fine; the 401 above was history
If a new message also fails with 401, then it’s real — and nine times out of ten the cause is the
shadowing trap below, not a dead token. (A startup notice or a “trust this folder” prompt can also swallow
your first message — press Esc then Enter to clear it, then send.)
When a live 401 hits every session: the shadowing trap
Claude Code can hold two credentials at once: a durable token (CLAUDE_CODE_OAUTH_TOKEN in the
environment, long-lived) and a stored browser-login (claude auth login — expires in hours, and its
auto-refresh silently breaks when several apps share it). Interactive sessions prefer the stored login and
don’t fall back when it’s expired — so the moment it expires, every session on the machine parks at
Please run /login, even though the durable token is perfectly valid. It looks like a mysterious
machine-wide failure that “comes back” after every apparent fix, because a fresh browser login fixes it for
exactly one expiry cycle.
The cure and the prevention are the same:
claude auth logout # remove the stored login; sessions fall through to the durable token
Then restart the stale sessions. On machines that run on a durable token, avoid claude auth login
entirely — and beware validating auth with claude -p: it silently falls back between credential stores,
so it can report success while every interactive session is dead. Probe the token itself instead (an
end-to-end message, or a direct API call).
Ready-made versions of all of this — amux-reauth (probe the token directly, clear a shadowing login,
restart the session) and check-claude-auth (read-only diagnosis) — live in the
amux-nui-public repo (opens in new tab)
alongside amux-convos.
The script
amux-convos is a small wrapper over the transcript files. Save it on your PATH as amux-convos:
#!/usr/bin/env bash
# amux-convos — list (and optionally resume) a session's past conversations, newest
# first, each with a snippet of the last thing you said so you can tell them apart.
set -uo pipefail
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
die(){ echo "amux-convos: $*" >&2; exit 1; }
name="" ; top=0 ; use_less=0 ; full=0 ; resume_idx="" ; cmd_idx=""
while [ $# -gt 0 ]; do
case "$1" in
-n|--top) shift; top=${1:-0} ;;
--less) use_less=1 ;;
--full) full=1 ;;
--resume) shift; resume_idx=${1:-} ;;
--cmd) shift; cmd_idx=${1:-} ;;
-h|--help) grep -m1 '^# amux-convos' "$0"; exit 0 ;;
-*) die "unknown flag: $1" ;;
*) [ -z "$name" ] && name="$1" || die "unexpected argument: $1" ;;
esac
shift
done
[ -n "$name" ] || die "usage: amux-convos <session> [-n N] [--less] [--resume N]"
# an amux session records its working directory; find its Claude Code project dir
envf="$HOME/.amux/sessions/$name.env"
[ -f "$envf" ] || die "no such amux session: '$name' (see 'amux ls')"
ccdir=$( . "$envf" >/dev/null 2>&1; printf %s "${CC_DIR:-}" )
[ -n "$ccdir" ] || die "session '$name' has no working directory recorded"
proj="$HOME/.claude/projects/$(printf %s "$ccdir" | sed 's/[/._]/-/g')"
[ -d "$proj" ] || proj=$(ls -d "$HOME"/.claude/projects/*"$(basename "$ccdir")" 2>/dev/null | head -1)
{ [ -n "$proj" ] && [ -d "$proj" ]; } || die "no conversations found for '$name'"
uuid_at() { ls -t "$proj"/*.jsonl 2>/dev/null | sed -n "${1}p" | while read -r f; do basename "$f" .jsonl; done; }
if [ -n "$resume_idx" ] || [ -n "$cmd_idx" ]; then
idx=${resume_idx:-$cmd_idx}; u=$(uuid_at "$idx"); [ -n "$u" ] || die "no conversation #$idx"
if [ -n "$cmd_idx" ]; then echo "amux start $name --resume $u"; exit 0; fi
echo "resuming '$name' conversation #$idx ($u)…"
amux stop "$name" >/dev/null 2>&1; sleep 1
exec amux start "$name" --resume "$u"
fi
python3 - "$proj" "$top" "$full" <<'PY' | { [ "$use_less" = 1 ] && less -R || cat; }
import sys, os, glob, json, datetime
proj, top, full = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
files = sorted(glob.glob(os.path.join(proj, "*.jsonl")), key=os.path.getmtime, reverse=True)
if top > 0: files = files[:top]
def last_user(f):
lu=None; n=0; ts=None
for line in open(f, errors="ignore"):
line=line.strip()
if not line: continue
try: o=json.loads(line)
except: continue
if o.get("timestamp"): ts=o["timestamp"]
m=o.get("message") or {}; role=m.get("role") or o.get("type")
if role in ("user","assistant"): n+=1
if role=="user":
c=m.get("content")
if isinstance(c,list):
for p in c:
if isinstance(p,dict) and p.get("type")=="text": lu=p.get("text")
elif isinstance(c,str): lu=c
return lu, n, ts
for i,f in enumerate(files,1):
uuid=os.path.basename(f)[:-6]; lu,n,ts=last_user(f)
when=(datetime.datetime.fromisoformat(ts.replace("Z","+00:00")).astimezone().strftime("%Y-%m-%d %H:%M")
if ts else "")
snip=(lu or "").strip().replace("\n"," ")
if not full and len(snip)>80: snip=snip[:80]+"…"
print(f"{i:>2}. {when} {n:>4} msgs {uuid}")
if snip: print(f' "{snip}"')
PY
It only reads the transcript files (except --resume, which stops and restarts the session), so it’s safe to
run against anything amux ls shows.
Source: content/guides/manage-conversations.md · maintained in the nuilab-agenticai repository.