Scan every project for broken transcripts, and stop crying wolf
repair-session only ever listed the project you were standing in, which is a poor place to start from when the thing you cannot resume is a session whose project you no longer remember. --all drops the working-directory scoping and checks all of them. (A named session id was already looked up across every project; only the listing was scoped.) The first version of that scan reported 16 of 59 transcripts as unresumable, which was true in the narrowest sense and useless in every other. Checking them: 5 had never received an assistant reply at all, and 10 had run start to finish on a gateway, so every id in them is that provider's by design. Those resume perfectly well under the provider they were born on, have nothing to truncate back to, and are only a problem if you try to resume them as Anthropic. Calling either of them damage buries the one case that is. So the verdict now carries a kind - healthy, repairable, gateway-native, synthetic-only, no-messages - and only `repairable` is reported or acted on: a transcript with a genuine msg_ message and junk after it. Across the same 59 it now correctly reports nothing to repair, while still classifying the real corrupted transcript from the incident as repairable with 108 lines to drop. A gateway-native session asked about directly now says what it is and that there is nothing to fix, rather than failing with "no Anthropic-issued message".
This commit is contained in:
@@ -343,11 +343,27 @@ There is no supported way back from that. The transcript has to be rolled back
|
|||||||
to the last message Anthropic issued:
|
to the last message Anthropic issued:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude-mode repair-session # list transcripts here and their state
|
claude-mode repair-session # transcripts for this project
|
||||||
|
claude-mode repair-session --all # every project, problems only
|
||||||
claude-mode repair-session <session-id> # show what it would cut
|
claude-mode repair-session <session-id> # show what it would cut
|
||||||
claude-mode repair-session <session-id> --apply
|
claude-mode repair-session <session-id> --apply
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Scope, since it is not obvious: a bare listing covers only the project you are
|
||||||
|
standing in (walking up from the current directory to find it), while a **named
|
||||||
|
session id is looked up across every project** — you rarely remember which
|
||||||
|
project a session you cannot resume belonged to. `--all` drops the scoping
|
||||||
|
entirely.
|
||||||
|
|
||||||
|
`--all` reports only what is actually actionable, which matters more than it
|
||||||
|
sounds. Of 59 transcripts here it initially flagged 16; on inspection 5 had
|
||||||
|
simply never received a reply, and 10 had run start-to-finish on a gateway, so
|
||||||
|
every id in them is that provider's by design. Those resume perfectly well under
|
||||||
|
the provider they were born on, have nothing to truncate back to, and are only a
|
||||||
|
problem if you try to resume them as Anthropic. Neither is damage, so neither is
|
||||||
|
listed. Only a transcript that has a genuine `msg_` message *and* junk after it
|
||||||
|
is something this can or should touch.
|
||||||
|
|
||||||
### The cut turns are not thrown away
|
### The cut turns are not thrown away
|
||||||
|
|
||||||
Truncating is the mechanical fix, but the turns being cut are the work itself —
|
Truncating is the mechanical fix, but the turns being cut are the work itself —
|
||||||
|
|||||||
+44
-3
@@ -2122,18 +2122,52 @@ cm_project_dir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_repair_session() {
|
cmd_repair_session() {
|
||||||
local target='' apply=0 reinject=1 dir='' a file verdict age
|
local target='' apply=0 reinject=1 scan_all=0 dir='' a file verdict age
|
||||||
for a in "$@"; do
|
for a in "$@"; do
|
||||||
case "$a" in
|
case "$a" in
|
||||||
--apply) apply=1 ;;
|
--apply) apply=1 ;;
|
||||||
--dry-run) apply=0 ;;
|
--dry-run) apply=0 ;;
|
||||||
--no-reinject) reinject=0 ;;
|
--no-reinject) reinject=0 ;;
|
||||||
--list) target='--list' ;;
|
--list) target='--list' ;;
|
||||||
|
--all) scan_all=1 ;;
|
||||||
-*) err "unknown option '$a'"; return 1 ;;
|
-*) err "unknown option '$a'"; return 1 ;;
|
||||||
*) target="$a" ;;
|
*) target="$a" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# A session you need to repair is one you could not resume, which is a poor
|
||||||
|
# position from which to remember which project it belonged to. --all drops
|
||||||
|
# the working-directory scoping and reports only what is actually broken.
|
||||||
|
if [ "$scan_all" -eq 1 ]; then
|
||||||
|
head_ 'scanning every session transcript'
|
||||||
|
local f v bad=0 total=0
|
||||||
|
for f in "$CM_SETTINGS_DIR"/projects/*/*.jsonl; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
total=$((total + 1))
|
||||||
|
v="$("$PY" "$JSON" repair-session "$f" 2>/dev/null)" || continue
|
||||||
|
printf '%s' "$v" | "$PY" -c "
|
||||||
|
import json,sys,os
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
# Only 'repairable' is damage this tool can or should act on.
|
||||||
|
if d['kind'] != 'repairable': raise SystemExit(0)
|
||||||
|
print(' %-38s %s' % (os.path.basename(d['path'])[:-6], os.path.basename(os.path.dirname(d['path']))))
|
||||||
|
print(' would drop %d line(s); %d foreign id(s)' % (d['dropLines'], len(d['foreignIds'])))
|
||||||
|
raise SystemExit(9)
|
||||||
|
" && continue
|
||||||
|
bad=$((bad + 1))
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
if [ "$bad" -eq 0 ]; then
|
||||||
|
ok "nothing to repair across $total transcript(s)"
|
||||||
|
else
|
||||||
|
warn "$bad of $total transcript(s) were cut short by a mode switch"
|
||||||
|
printf ' %sclaude-mode repair-session <id> --apply%s\n' "$C_DIM" "$C_RESET"
|
||||||
|
fi
|
||||||
|
printf ' %ssessions that ran entirely on a gateway are not listed: they carry that%s\n' "$C_DIM" "$C_RESET"
|
||||||
|
printf ' %sprovider'"'"'s ids by design and resume fine under it%s\n' "$C_DIM" "$C_RESET"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Claude Code keys transcripts by the directory the session was started in,
|
# Claude Code keys transcripts by the directory the session was started in,
|
||||||
# which is rarely the one you are standing in when you come to fix it. Walk
|
# which is rarely the one you are standing in when you come to fix it. Walk
|
||||||
# up first, and for a named session fall back to looking through every
|
# up first, and for a named session fall back to looking through every
|
||||||
@@ -2172,6 +2206,7 @@ print(' %-40s %s' % (os.path.basename(d['path'])[:-6], state))
|
|||||||
"
|
"
|
||||||
done
|
done
|
||||||
printf '\n %sclaude-mode repair-session <session-id> --apply%s\n' "$C_DIM" "$C_RESET"
|
printf '\n %sclaude-mode repair-session <session-id> --apply%s\n' "$C_DIM" "$C_RESET"
|
||||||
|
printf ' %s--all checks every project, not just this one%s\n' "$C_DIM" "$C_RESET"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -2202,9 +2237,15 @@ print()
|
|||||||
if d['healthy']:
|
if d['healthy']:
|
||||||
print(' %sok %s last message is Anthropic-issued; nothing to repair' % (G,X))
|
print(' %sok %s last message is Anthropic-issued; nothing to repair' % (G,X))
|
||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
|
if d['kind'] == 'gateway-native':
|
||||||
|
prov = (d['foreignIds'][0]['model'] or 'a gateway') if d['foreignIds'] else 'a gateway'
|
||||||
|
print(' %sok %s this session ran entirely on %s' % (G,X,prov))
|
||||||
|
print(' %sits ids come from that provider by design; it resumes under it, not%s' % (D,X))
|
||||||
|
print(' %sunder Anthropic. There is nothing here to repair.%s' % (D,X))
|
||||||
|
raise SystemExit(0)
|
||||||
if not d['repairable']:
|
if not d['repairable']:
|
||||||
print(' %sFAIL%s no Anthropic-issued message anywhere in this transcript' % (R,X))
|
print(' %sok %s this transcript has no assistant replies to resume from' % (G,X))
|
||||||
raise SystemExit(1)
|
raise SystemExit(0)
|
||||||
for f in d['foreignIds']:
|
for f in d['foreignIds']:
|
||||||
print(' %swarn%s line %d carries a %s id from %s' % (Y,X,f['line'],f['id'].split('-')[0]+'-',f['model'] or 'another provider'))
|
print(' %swarn%s line %d carries a %s id from %s' % (Y,X,f['line'],f['id'].split('-')[0]+'-',f['model'] or 'another provider'))
|
||||||
n = sum(1 for s in d['syntheticIds'] if s['apiError'] and s['line'] > d['lastGoodLine'])
|
n = sum(1 for s in d['syntheticIds'] if s['apiError'] and s['line'] > d['lastGoodLine'])
|
||||||
|
|||||||
@@ -750,6 +750,23 @@ def cmd_repair_session(argv):
|
|||||||
if lines[i].strip() and _msg_id(_safe(lines[i])) is not None]
|
if lines[i].strip() and _msg_id(_safe(lines[i])) is not None]
|
||||||
healthy = (last_good >= 0 and not tail_ids)
|
healthy = (last_good >= 0 and not tail_ids)
|
||||||
|
|
||||||
|
# Not every transcript without an Anthropic id is damaged, and saying so
|
||||||
|
# turns the scan into noise. A session that ran start to finish on a gateway
|
||||||
|
# has `gen-` ids throughout by design: it resumes perfectly well under the
|
||||||
|
# provider it was born on, has nothing to truncate back to, and is only a
|
||||||
|
# problem if you try to resume it as Anthropic. Likewise a session that never
|
||||||
|
# got a reply at all has no ids and nothing wrong with it.
|
||||||
|
if healthy:
|
||||||
|
kind = "healthy"
|
||||||
|
elif last_good >= 0:
|
||||||
|
kind = "repairable"
|
||||||
|
elif foreign:
|
||||||
|
kind = "gateway-native"
|
||||||
|
elif synthetic:
|
||||||
|
kind = "synthetic-only"
|
||||||
|
else:
|
||||||
|
kind = "no-messages"
|
||||||
|
|
||||||
out = {
|
out = {
|
||||||
"path": path,
|
"path": path,
|
||||||
"lines": len(lines),
|
"lines": len(lines),
|
||||||
@@ -758,6 +775,7 @@ def cmd_repair_session(argv):
|
|||||||
"foreignIds": foreign,
|
"foreignIds": foreign,
|
||||||
"syntheticIds": synthetic,
|
"syntheticIds": synthetic,
|
||||||
"healthy": healthy,
|
"healthy": healthy,
|
||||||
|
"kind": kind,
|
||||||
"repairable": (not healthy) and last_good >= 0,
|
"repairable": (not healthy) and last_good >= 0,
|
||||||
"applied": False,
|
"applied": False,
|
||||||
"backup": "",
|
"backup": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user