@@ -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
11501152function 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