diff --git a/.gitignore b/.gitignore index e38b0e9bb..6df539057 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,13 @@ # ── Compiled test output ── dist-test/ +# ── Compiled output in src/ (should only contain .ts source) ── +src/**/*.js +src/**/*.js.map +src/**/*.d.ts +src/**/*.d.ts.map +!src/**/*.test.js + # ── GSD project state (development-only, lives in worktree branches) ── package-lock.json .claude/ @@ -42,6 +49,9 @@ tmp/ packages/*/dist/ packages/*/node_modules/ +# ── Scratch/WIP files ── +preflight-script.ts + # ── GSD baseline (auto-generated) ── dist/ !/pkg/dist/modes/ @@ -55,6 +65,7 @@ TODOS.md .planning/ .audits/ docs/coherence-audit/ +.plans/ # ── GSD project state (per-worktree, never committed) ── .gsd/ diff --git a/src/resources/extensions/gsd/error-classifier.ts b/src/resources/extensions/gsd/error-classifier.ts index d228cfa34..604167451 100644 --- a/src/resources/extensions/gsd/error-classifier.ts +++ b/src/resources/extensions/gsd/error-classifier.ts @@ -48,7 +48,9 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fet const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i; // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first). const CONNECTION_RE = /terminated|connection.?refused|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i; -const STREAM_RE = /Unexpected end of JSON|Unexpected token.*JSON|Expected.*in JSON|Unterminated.*in JSON|SyntaxError.*JSON/i; +// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+". +// This eliminates the need to enumerate every error message variant individually. +const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i; const RESET_DELAY_RE = /reset in (\d+)s/i; /** @@ -91,9 +93,6 @@ export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorCla } // 4. Stream truncation — downstream symptom of connection drop - // Checked before server/connection because JSON parse errors can contain - // substrings like "position 500" (matches SERVER_RE) or "Unterminated" - // (matches CONNECTION_RE's "terminated" pattern). if (STREAM_RE.test(errorMsg)) { return { kind: "stream", retryAfterMs: retryAfterMs ?? 15_000 }; } diff --git a/src/resources/extensions/gsd/tests/terminated-transient.test.ts b/src/resources/extensions/gsd/tests/terminated-transient.test.ts index f15223f60..84c0c8db0 100644 --- a/src/resources/extensions/gsd/tests/terminated-transient.test.ts +++ b/src/resources/extensions/gsd/tests/terminated-transient.test.ts @@ -82,3 +82,47 @@ test("#2572: 'SyntaxError' with JSON context (truncated stream) is transient", ( assert.equal(isTransient(result), true, "'SyntaxError...JSON' should be transient"); assert.equal(result.kind, "stream", "JSON parse errors are stream kind"); }); + +// --- Catch-all: all V8 JSON.parse variants matched by "in JSON at position" --- + +test("V8 JSON.parse: 'No number after minus sign in JSON' is transient (#2882)", () => { + const result = classifyError("No number after minus sign in JSON at position 42"); + assert.equal(isTransient(result), true); + assert.equal(result.kind, "stream"); +}); + +test("V8 JSON.parse: 'Expected property value after colon' is transient", () => { + const result = classifyError("Expected ',' or '}' after property value in JSON at position 108"); + assert.equal(isTransient(result), true); + assert.equal(result.kind, "stream"); +}); + +test("V8 JSON.parse: 'Bad control character in string literal' is transient", () => { + const result = classifyError("Bad control character in string literal in JSON at position 5"); + assert.equal(isTransient(result), true); + assert.equal(result.kind, "stream"); +}); + +test("V8 JSON.parse: 'Bad escaped character' is transient", () => { + const result = classifyError("Bad escaped character in JSON at position 17"); + assert.equal(isTransient(result), true); + assert.equal(result.kind, "stream"); +}); + +test("V8 JSON.parse: 'Unexpected number' is transient", () => { + const result = classifyError("Unexpected number in JSON at position 0"); + assert.equal(isTransient(result), true); + assert.equal(result.kind, "stream"); +}); + +test("V8 JSON.parse: 'Unexpected string' is transient", () => { + const result = classifyError("Unexpected string in JSON at position 12"); + assert.equal(isTransient(result), true); + assert.equal(result.kind, "stream"); +}); + +test("V8 JSON.parse with line/column suffix is transient", () => { + const result = classifyError("Unexpected token x in JSON at position 99 (line 3 column 14)"); + assert.equal(isTransient(result), true); + assert.equal(result.kind, "stream"); +});