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.

Updated Jul 29, 2026

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

The only reliable test is end to end: send the session a new message and watch for a real answer. A transcript full of old 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.