diff --git a/src/resources/extensions/gsd/provider-error-pause.ts b/src/resources/extensions/gsd/provider-error-pause.ts index 92cc1fa0c..7a5414999 100644 --- a/src/resources/extensions/gsd/provider-error-pause.ts +++ b/src/resources/extensions/gsd/provider-error-pause.ts @@ -46,6 +46,15 @@ export function classifyProviderError(errorMsg: string): { return { isTransient: true, isRateLimit: false, suggestedDelayMs: 15_000 }; // 15s for connection errors } + // Stream-truncation JSON parse errors — transient (#2572). + // When the API stream is cut mid-chunk, pi tries to reassemble the partial + // tool-call JSON and gets a SyntaxError. This is the downstream symptom of + // a connection drop — same root cause as ECONNRESET, one layer up. + const isMalformedStream = /Unexpected end of JSON|Unexpected token.*JSON|Expected double-quoted property name|SyntaxError.*JSON/i.test(errorMsg); + if (isMalformedStream) { + return { isTransient: true, isRateLimit: false, suggestedDelayMs: 15_000 }; // 15s, same as connection errors + } + // Unknown error — treat as permanent (user reviews) return { isTransient: false, isRateLimit: false, suggestedDelayMs: 0 }; } diff --git a/src/resources/extensions/gsd/tests/terminated-transient.test.ts b/src/resources/extensions/gsd/tests/terminated-transient.test.ts index 066bebd3f..52d178625 100644 --- a/src/resources/extensions/gsd/tests/terminated-transient.test.ts +++ b/src/resources/extensions/gsd/tests/terminated-transient.test.ts @@ -47,3 +47,27 @@ test("#2309: rate limits are still transient", () => { assert.equal(rlResult.isTransient, true, "rate limits are still transient"); assert.equal(rlResult.isRateLimit, true, "rate limits are flagged as rate limits"); }); + +// --- #2572: stream-truncation JSON parse errors should be transient --- + +test("#2572: 'Expected double-quoted property name' (truncated stream) is transient", () => { + const result = classifyProviderError("Expected double-quoted property name in JSON at position 23 (line 1 column 24)"); + assert.equal(result.isTransient, true, "truncated-stream JSON parse error should be transient"); + assert.equal(result.isRateLimit, false, "not a rate limit"); + assert.equal(result.suggestedDelayMs, 15_000, "should use 15s backoff like connection errors"); +}); + +test("#2572: 'Unexpected end of JSON input' (truncated stream) is transient", () => { + const result = classifyProviderError("Unexpected end of JSON input"); + assert.equal(result.isTransient, true, "'Unexpected end of JSON input' should be transient"); +}); + +test("#2572: 'Unexpected token' in JSON (truncated stream) is transient", () => { + const result = classifyProviderError("Unexpected token < in JSON at position 0"); + assert.equal(result.isTransient, true, "'Unexpected token in JSON' should be transient"); +}); + +test("#2572: 'SyntaxError' with JSON context (truncated stream) is transient", () => { + const result = classifyProviderError("SyntaxError: JSON.parse: unexpected character at line 1 column 1"); + assert.equal(result.isTransient, true, "'SyntaxError...JSON' should be transient"); +});