Skip to content

Commit a152d83

Browse files
committed
fix: repair function now removes API Error messages from sessions (v0.8.13)
When switching API providers, stale API Error assistant messages corrupt the conversation context causing "unexpected end of JSON input" 400 errors on resume. The repair function now strips these error lines along with their associated retry attempts and scaffolding lines.
1 parent 4c27b60 commit a152d83

File tree

2 files changed

+105
-2
lines changed

2 files changed

+105
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "claude-code-exporter",
33
"displayName": "Claude Code Exporter — Claude Code, Codex & Cursor Conversation History",
44
"description": "Export AI coding agent conversations to Markdown. Supports Claude Code (→ .claude-code-history/), OpenAI Codex (→ .codex-history/), and Cursor Composer (→ .cursor-history/). Features: auto-watch, session inject/import for claude --resume, and repair of thinking-block signature errors when switching API providers.",
5-
"version": "0.8.12",
5+
"version": "0.8.13",
66
"publisher": "myoontyee",
77
"engines": {
88
"vscode": "^1.85.0"

src/extension.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1141,11 +1141,13 @@ function updateStatusBar(
11411141
}
11421142

11431143
// ─── Thinking-signature repair ─────────────────────────────────────────────────
1144-
// Fixes four classes of provider-compatibility errors:
1144+
// Fixes five classes of provider-compatibility errors:
11451145
// 1. thinking blocks → text blocks (removes signature, converts type)
11461146
// 2. model names with "-thinking" suffix → strip suffix
11471147
// 3. misplaced tool_results — parallel tool_use results that landed in wrong turn
11481148
// 4. orphaned tool_use blocks (no matching tool_result anywhere) → inject synthetic
1149+
// 5. API Error messages — remove assistant lines containing "API Error:" text
1150+
// (these corrupt the conversation context and cause 400 errors on resume)
11491151

11501152
function repairThinkingSignatures(jsonlPath: string): number {
11511153
const raw = fs.readFileSync(jsonlPath, 'utf8');
@@ -1185,6 +1187,106 @@ function repairThinkingSignatures(jsonlPath: string): number {
11851187
try { return JSON.parse(line) as Record<string, unknown>; } catch { return null; }
11861188
});
11871189

1190+
// Pass 0: mark API Error assistant messages for removal
1191+
// These contain raw HTTP error JSON that corrupts context on resume
1192+
const removedIndices = new Set<number>();
1193+
for (let i = 0; i < parsed.length; i++) {
1194+
const obj = parsed[i];
1195+
if (!obj) continue;
1196+
const role = (obj.message as Record<string, unknown> | undefined)?.role;
1197+
if (role !== 'assistant') continue;
1198+
const content = (obj.message as Record<string, unknown>).content;
1199+
// Check if ALL blocks are API Error text
1200+
if (Array.isArray(content)) {
1201+
const allApiError = content.length > 0 && content.every((b: Record<string, unknown>) =>
1202+
b.type === 'text' && typeof b.text === 'string' && (b.text as string).startsWith('API Error:')
1203+
);
1204+
if (allApiError) {
1205+
removedIndices.add(i);
1206+
fixed++;
1207+
}
1208+
} else if (typeof content === 'string' && content.startsWith('API Error:')) {
1209+
removedIndices.add(i);
1210+
fixed++;
1211+
}
1212+
}
1213+
1214+
// Also remove file-history-snapshot and queue-operation lines that are
1215+
// adjacent to (immediately before/after) removed API error lines,
1216+
// as they are part of the failed turn
1217+
for (const idx of [...removedIndices]) {
1218+
// Look backward: remove preceding file-history-snapshot, custom-title, queue-operation
1219+
for (let j = idx - 1; j >= 0; j--) {
1220+
const o = parsed[j];
1221+
if (!o) continue;
1222+
const t = o.type as string;
1223+
if (t === 'file-history-snapshot' || t === 'custom-title' || t === 'queue-operation') {
1224+
removedIndices.add(j);
1225+
} else {
1226+
break;
1227+
}
1228+
}
1229+
// Look forward: remove following queue-operation, custom-title
1230+
for (let j = idx + 1; j < parsed.length; j++) {
1231+
const o = parsed[j];
1232+
if (!o) continue;
1233+
const t = o.type as string;
1234+
if (t === 'queue-operation' || t === 'custom-title') {
1235+
removedIndices.add(j);
1236+
} else {
1237+
break;
1238+
}
1239+
}
1240+
}
1241+
1242+
// Remove duplicate user messages that precede API errors
1243+
// Pattern: user says X → API Error → user says X again → API Error again
1244+
// Keep only the last user attempt
1245+
for (let i = 0; i < parsed.length; i++) {
1246+
if (removedIndices.has(i)) continue;
1247+
const obj = parsed[i];
1248+
if (!obj) continue;
1249+
const role = (obj.message as Record<string, unknown> | undefined)?.role;
1250+
if (role !== 'user') continue;
1251+
// Check if the next non-removed assistant line is an API Error (already removed)
1252+
let nextAssistantRemoved = false;
1253+
for (let j = i + 1; j < parsed.length; j++) {
1254+
const nxt = parsed[j];
1255+
if (!nxt) continue;
1256+
const nxtRole = (nxt.message as Record<string, unknown> | undefined)?.role;
1257+
if (nxtRole === 'assistant') {
1258+
if (removedIndices.has(j)) nextAssistantRemoved = true;
1259+
break;
1260+
}
1261+
if (nxt.type === 'user' || nxtRole === 'user') break;
1262+
}
1263+
if (nextAssistantRemoved) {
1264+
removedIndices.add(i);
1265+
// Also remove file-history-snapshot before this user line
1266+
for (let j = i - 1; j >= 0; j--) {
1267+
const o = parsed[j];
1268+
if (!o) continue;
1269+
const t = o.type as string;
1270+
if (t === 'file-history-snapshot' || t === 'queue-operation') {
1271+
removedIndices.add(j);
1272+
} else {
1273+
break;
1274+
}
1275+
}
1276+
// And after
1277+
for (let j = i + 1; j < parsed.length; j++) {
1278+
const o = parsed[j];
1279+
if (!o) continue;
1280+
const t = o.type as string;
1281+
if (t === 'file-history-snapshot') {
1282+
removedIndices.add(j);
1283+
} else {
1284+
break;
1285+
}
1286+
}
1287+
}
1288+
}
1289+
11881290
// Pass 1: fix thinking blocks and model names
11891291
for (let i = 0; i < parsed.length; i++) {
11901292
const obj = parsed[i];
@@ -1259,6 +1361,7 @@ function repairThinkingSignatures(jsonlPath: string): number {
12591361
// Pass 3: find truly orphaned tool_use (no tool_result anywhere), inject synthetic
12601362
const resultLines: string[] = [];
12611363
for (let i = 0; i < parsed.length; i++) {
1364+
if (removedIndices.has(i)) continue; // skip API Error lines and their scaffolding
12621365
const obj = parsed[i];
12631366
resultLines.push(obj ? JSON.stringify(obj) : lines[i]);
12641367
if (!obj || getRole(obj) !== 'assistant') continue;

0 commit comments

Comments
 (0)