From 81059b123e44dd61edf7cd3fa4a01c8fdc018c6b Mon Sep 17 00:00:00 2001 From: mirsella Date: Mon, 15 Dec 2025 15:44:10 +0100 Subject: [PATCH] fix: handle corrupted JSON files gracefully with error logging --- packages/opencode/src/cli/cmd/stats.ts | 18 ++++++++++++++-- packages/opencode/src/format/formatter.ts | 10 ++++++--- packages/opencode/src/session/message-v2.ts | 15 +++++++++----- packages/opencode/src/storage/storage.ts | 23 ++++++++++++++++++--- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index f41b23ee9..6dca27696 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -64,13 +64,27 @@ async function getAllSessions(): Promise { const sessions: Session.Info[] = [] const projectKeys = await Storage.list(["project"]) - const projects = await Promise.all(projectKeys.map((key) => Storage.read(key))) + const projects = await Promise.all( + projectKeys.map((key) => + Storage.read(key).catch((e) => { + console.error(`Skipping corrupted project ${key.join("/")}:`, e) + return null + }), + ), + ) for (const project of projects) { if (!project) continue const sessionKeys = await Storage.list(["session", project.id]) - const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read(key))) + const projectSessions = await Promise.all( + sessionKeys.map((key) => + Storage.read(key).catch((e) => { + console.error(`Skipping corrupted session ${key.join("/")}:`, e) + return null + }), + ), + ) for (const session of projectSessions) { if (session) { diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index c4e7c9ee8..2b6a2b38c 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -66,9 +66,13 @@ export const prettier: Info = { async enabled() { const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree) for (const item of items) { - const json = await Bun.file(item).json() - if (json.dependencies?.prettier) return true - if (json.devDependencies?.prettier) return true + try { + const json = await Bun.file(item).json() + if (json.dependencies?.prettier) return true + if (json.devDependencies?.prettier) return true + } catch (e) { + console.error(`Skipping corrupted package.json ${item}:`, e) + } } return false }, diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 76162c797..dbae8a137 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -542,10 +542,14 @@ export namespace MessageV2 { export const stream = fn(Identifier.schema("session"), async function* (sessionID) { const list = await Array.fromAsync(await Storage.list(["message", sessionID])) for (let i = list.length - 1; i >= 0; i--) { - yield await get({ - sessionID, - messageID: list[i][2], - }) + try { + yield await get({ + sessionID, + messageID: list[i][2], + }) + } catch (e) { + console.error(`Skipping corrupted message ${list[i][2]}:`, e) + } } }) @@ -608,7 +612,7 @@ export namespace MessageV2 { }, { cause: e }, ).toObject() - case APICallError.isInstance(e): + case APICallError.isInstance(e): { const message = iife(() => { let msg = e.message const transformed = ProviderTransform.error(ctx.providerID, e) @@ -641,6 +645,7 @@ export namespace MessageV2 { }, { cause: e }, ).toObject() + } case e instanceof Error: return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject() default: diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 8b4042ea1..0dde8da5f 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -170,8 +170,15 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.read(target) - const result = await Bun.file(target).json() - return result as T + try { + const result = await Bun.file(target).json() + return result as T + } catch (e) { + if (e instanceof SyntaxError) { + throw new Error(`Failed to parse JSON in file ${target}: ${e.message}`) + } + throw e + } }) } @@ -180,7 +187,17 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.write(target) - const content = await Bun.file(target).json() + let content: T + try { + content = await Bun.file(target).json() + } catch (e) { + if (e instanceof SyntaxError) { + console.error(`Corrupted JSON in ${target}, resetting to empty object:`, e.message) + content = {} as T + } else { + throw e + } + } fn(content) await Bun.write(target, JSON.stringify(content, null, 2)) return content as T