From bbd9468c783b9d7201d2fefc1e57aca6f6a052c8 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:20:41 +0100 Subject: [PATCH] fix: classify stream-truncation JSON parse errors as transient (#2636) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the API stream is truncated mid-chunk, pi reassembles the partial tool-call JSON and gets a SyntaxError (e.g. "Expected double-quoted property name", "Unexpected end of JSON input"). classifyProviderError() did not match these patterns and fell through to the "unknown = permanent" default, pausing auto-mode indefinitely instead of retrying. These JSON parse errors are the downstream symptom of a connection drop — same root cause as ECONNRESET, one layer up. Add an isMalformedStream guard that matches common JSON SyntaxError patterns and classifies them as transient with the same 15s backoff as connection errors. Closes #2572 --- .../extensions/gsd/provider-error-pause.ts | 9 +++++++ .../gsd/tests/terminated-transient.test.ts | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) 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"); +});