diff --git a/bin/rclaude b/bin/rclaude index 457c840..c24b5d7 100755 --- a/bin/rclaude +++ b/bin/rclaude @@ -187,9 +187,19 @@ _filter_sessions_to_uuids() { | awk -F'\t' -v r="^($_re)$" '$3 ~ r' } -# Build a `host\tuuid\tname` table of every session that has a display name -# set via `claude -n `. Used to enrich the resume picker so names show -# next to each row. +# Durable name index — persistent record of (host, uuid, name) tuples for +# every session display name we've ever observed. Lives on plum, survives +# remote-host reboots. Source of truth for name_search_on alongside the +# live per-pid sessions/*.json files. +_NAME_INDEX=${XDG_DATA_HOME:-$HOME/.local/share}/rclaude/named-sessions.tsv +mkdir -p "$(dirname "$_NAME_INDEX")" 2>/dev/null +[ -f "$_NAME_INDEX" ] || touch "$_NAME_INDEX" 2>/dev/null + +# Build a `host\tuuid\tname` table of every session display name we know +# about. Combines live observation (ssh into each host, scan +# ~/.claude/sessions/*.json) with the durable on-disk index — so even after +# a remote reboot wipes the per-pid metadata, names we saw previously stay +# searchable. Live observations are upserted into the index every call. build_name_map() { _py='import json, os, glob for f in glob.glob(os.path.expanduser("~/.claude/sessions/*.json")): @@ -198,6 +208,8 @@ for f in glob.glob(os.path.expanduser("~/.claude/sessions/*.json")): sid = d.get("sessionId") or "" name = d.get("name") or "" if sid and name: print(f"{sid}\t{name}")' + # Live pass: collect current observations into a temp file. + _live=$(mktemp /tmp/rclaude-namemap-live.XXXXXX 2>/dev/null || echo /tmp/rclaude-namemap-live.$$) scan_hosts | while IFS= read -r _h; do if is_local "$_h"; then python3 -c "$_py" 2>/dev/null @@ -205,8 +217,19 @@ for f in glob.glob(os.path.expanduser("~/.claude/sessions/*.json")): ssh -o BatchMode=yes -o ConnectTimeout=3 "$_h" "python3 -" 2>/dev/null <=2 {print host "\t" $1 "\t" $2}' + done > "$_live" + # Persist anything new into the durable index. Dedup by (host, uuid) + # keeping the most-recent name we've ever seen (later writes win). + if [ -s "$_live" ]; then + cat "$_NAME_INDEX" "$_live" \ + | awk -F'\t' 'NF>=3 { key=$1 SUBSEP $2; row[key]=$0 } END { for (k in row) print row[k] }' \ + > "${_NAME_INDEX}.tmp" \ + && mv -f "${_NAME_INDEX}.tmp" "$_NAME_INDEX" + fi + rm -f "$_live" + # Output: every row in the durable index. Picker uses this to enrich rows. + cat "$_NAME_INDEX" 2>/dev/null } # Cheap match by session display name (the `claude -n ` label, stored in @@ -224,6 +247,7 @@ for f in glob.glob(os.path.expanduser("~/.claude/sessions/*.json")): name = (d.get("name") or "").lower() sid = d.get("sessionId") or "" if pat in name and sid: print(sid)' + # Live pass — per-pid metadata on (vanishes on reboot). if is_local "$_host"; then _uuids=$(RCLAUDE_PAT="$_pat" python3 -c "$_py" 2>/dev/null || true) else @@ -233,7 +257,16 @@ $_py PYEOF ) fi - _filter_sessions_to_uuids "$_host" "$_uuids" + # Durable pass — names we've previously seen for THIS host, even if the + # per-pid file is gone now (reboot, claude crash, etc.). Case-insensitive + # substring match on the name column. + _pat_lc=$(printf %s "$_pat" | tr '[:upper:]' '[:lower:]') + _dur_uuids=$(awk -F'\t' -v h="$_host" -v p="$_pat_lc" ' + BEGIN { IGNORECASE = 1 } + $1 == h && index(tolower($3), p) > 0 { print $2 } + ' "$_NAME_INDEX" 2>/dev/null) + _all_uuids=$(printf '%s\n%s\n' "$_uuids" "$_dur_uuids" | sort -u | grep -v '^$' || true) + _filter_sessions_to_uuids "$_host" "$_all_uuids" } # Grep the full content of every Claude session JSONL on for .