From 0d8e6c26bb656664ac5f9cc3b7762af1c203d47a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 30 Mar 2026 06:58:47 -0500 Subject: [PATCH 1/2] fix(error-classifier): replace STREAM_RE whack-a-mole with catch-all V8 JSON.parse pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old STREAM_RE enumerated specific JSON error messages individually, missing variants like "No number after minus sign", "Bad control character", and "Expected ',' or '}' after property value" — causing auto-mode to permanently pause instead of retrying. Replace with a catch-all: all V8 JSON.parse errors end with "in JSON at position \d+", so one pattern covers every current and future variant. Keeps "Unexpected end of JSON" and "SyntaxError.*JSON" for older runtimes. Supersedes PR #3134. Refs #2882. --- .../extensions/gsd/error-classifier.ts | 4 +- .../gsd/tests/terminated-transient.test.ts | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/error-classifier.ts b/src/resources/extensions/gsd/error-classifier.ts index c63927b98..eb47d46c4 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 double-quoted property name|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; /** 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"); +}); From 977e6bf963a7101bce0cd09575d7d252ffd0124b Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 30 Mar 2026 07:12:57 -0500 Subject: [PATCH 2/2] chore(gitignore): exclude src/ build artifacts, scratch files, and .plans/ Compiled .js/.d.ts/.map files from dev builds were leaking into src/. Also ignores preflight-script.ts (scratch WIP) and .plans/ directory. --- .gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) 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/