{"record":{"id":"ebd5c4d7735c49e6","repo":"can1357/oh-my-pi","slug":"unterminated-string","errorCode":null,"errorMessage":"Unterminated string","messagePattern":"Unterminated string","errorType":"exception","errorClass":"SyntaxError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/json-parse.ts","lineNumber":442,"sourceCode":"\t\t\t\t\t\tout += String.fromCharCode(parseInt(hex, 16));\n\t\t\t\t\t\ti += 4;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tout += \"\\\\u\"; // invalid \\u — keep literal\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tout += `\\\\${s[i]}`; // invalid escape — keep backslash literal\n\t\t\t}\n\t\t\ti++;\n\t\t\trunStart = i;\n\t\t}\n\t\tout += s.slice(runStart, i);\n\t\tif (this.#partial) {\n\t\t\tthis.#i = i;\n\t\t\treturn out;\n\t\t}\n\t\tthrow new SyntaxError(\"Unterminated string\");\n\t}\n\n\t/** A quote closes a string only when the next non-space char ends a value. */\n\t#closesString(from: number): boolean {\n\t\tconst s = this.#s;\n\t\tlet k = from;\n\t\twhile (k < this.#n && isWhitespace(s.charCodeAt(k))) k++;\n\t\tif (k >= this.#n) return true;\n\t\tconst c = s[k];\n\t\treturn c === \",\" || c === \"}\" || c === \"]\" || c === \":\";\n\t}\n\n\t#number(): unknown {\n\t\tconst s = this.#s;\n\t\tconst start = this.#i;\n\t\twhile (this.#i < this.#n) {\n\t\t\tconst ch = s[this.#i];\n\t\t\tif (","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/json-parse.ts#L424-L460","documentation":"Thrown by RelaxedJson.#string in strict mode when end-of-input is reached without a closing quote. In partial mode an unterminated string is returned as-is (streaming auto-close); in strict mode it throws. Note the parser does try to recover unescaped inner quotes ('it's'), so this error means the string genuinely never terminates.","triggerScenarios":"parseJsonWithRepair with '\"abc', '{\"key\": \"value', or a quote whose closer was consumed as an escape target ('abc\\\\' — trailing backslash swallows the quote, runStart breaks, then EOI). Only in strict mode; parseStreamingJson never throws this.","commonSituations":"LLM tool-call output truncated mid-string by a token limit; a JSON string missing its closing quote in hand-written config; a payload cut by a fixed-size read or log clipping.","solutions":["Ensure the complete payload arrives before final parsing (check stream completion / content-length)","Use parseStreamingJson for incomplete input — unterminated strings are auto-closed","Use classifyJsonPrefix: a 'prefix' result means the string may still be closed by more data","Re-generate or re-request the output if a provider truncated it"],"exampleFix":"// before\nconst v = parseJsonWithRepair(`{\"msg\": \"hello`); // SyntaxError: Unterminated string\n// after\nconst v = parseStreamingJson(`{\"msg\": \"hello`); // {msg: 'hello'}","handlingStrategy":"validation","validationCode":"import { classifyJsonPrefix } from \"@oh-my-pi/pi-utils/json-parse\";\n// 'prefix' includes unterminated strings that more data may close; 'invalid' cannot be repaired\nconst state = classifyJsonPrefix(text);","typeGuard":null,"tryCatchPattern":"try {\n  const value = parseJsonWithRepair<T>(text);\n} catch (err) {\n  if (err instanceof SyntaxError && err.message === \"Unterminated string\") {\n    const partial = parseStreamingJson<T>(text); // auto-closes the string\n  } else throw err;\n}","preventionTips":["Await full stream completion before final-parsing","Use parseStreamingJson for streaming tool-call argument buffers","Avoid trailing backslashes in string data (they swallow the closing quote)","Use classifyJsonPrefix to distinguish still-open strings from dead input"],"tags":["json","parse","string","unterminated","truncated"],"backgroundTag":"json-unterminated-string","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}