diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2537f8949332..f56c8162d308 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -18,6 +18,7 @@ import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" import { Filesystem } from "../util/filesystem" +import { sanitizeJsonSurrogates } from "../util/sanitize-surrogates" // Direct imports for bundled providers import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock" @@ -1167,6 +1168,12 @@ export namespace Provider { } } + // Sanitize JSON-escaped lone surrogates that may exist in previously + // stored session data. See: https://github.com/anomalyco/opencode/issues/14630 + if (typeof opts.body === "string") { + opts.body = sanitizeJsonSurrogates(opts.body) + } + const res = await fetchFn(input, { ...opts, // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 158b83865dc4..dc16ba6580b5 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -186,7 +186,7 @@ export namespace SessionProcessor { state: { status: "completed", input: value.input ?? match.state.input, - output: value.output.output, + output: value.output.output?.toWellFormed?.() ?? value.output.output, metadata: value.output.metadata, title: value.output.title, time: { @@ -210,7 +210,7 @@ export namespace SessionProcessor { state: { status: "error", input: value.input ?? match.state.input, - error: (value.error as any).toString(), + error: (value.error as any).toString()?.toWellFormed?.() ?? (value.error as any).toString(), time: { start: match.state.time.start, end: Date.now(), diff --git a/packages/opencode/src/util/sanitize-surrogates.ts b/packages/opencode/src/util/sanitize-surrogates.ts new file mode 100644 index 000000000000..1e0a757fe522 --- /dev/null +++ b/packages/opencode/src/util/sanitize-surrogates.ts @@ -0,0 +1,17 @@ +// Regex that matches a JSON-escaped high surrogate (\uD800–\uDBFF) +// NOT followed by a JSON-escaped low surrogate (\uDC00–\uDFFF), +// or a JSON-escaped low surrogate NOT preceded by a high surrogate. +// +// This is necessary because JSON.stringify() (per ECMA-262) encodes lone +// surrogate code units as \uD8xx ASCII escapes, which are valid JavaScript +// but violate RFC 8259. Strict JSON parsers (Anthropic serde_json, OpenAI) +// reject these with errors like "no low surrogate in string". +const LONE_HIGH_SURROGATE = /\\u[dD][89aAbB][0-9a-fA-F]{2}(?!\\u[dD][cCdDeEfF][0-9a-fA-F]{2})/g +const LONE_LOW_SURROGATE = /(?