From 808de5836d224ae9730b17dab0e8a2f1e3fb8fe6 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 15 Feb 2026 14:54:59 +0900 Subject: [PATCH] feat: implement SQLite backend for replaceEmptyTextParts via HTTP PATCH --- .../session-recovery/storage/empty-text.ts | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/hooks/session-recovery/storage/empty-text.ts b/src/hooks/session-recovery/storage/empty-text.ts index 60edc1f77..53bee36b8 100644 --- a/src/hooks/session-recovery/storage/empty-text.ts +++ b/src/hooks/session-recovery/storage/empty-text.ts @@ -1,14 +1,17 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs" import { join } from "node:path" +import type { PluginInput } from "@opencode-ai/plugin" import { PART_STORAGE } from "../constants" -import type { StoredPart, StoredTextPart } from "../types" +import type { StoredPart, StoredTextPart, MessageData } from "../types" import { readMessages } from "./messages-reader" import { readParts } from "./parts-reader" -import { log, isSqliteBackend } from "../../../shared" +import { log, isSqliteBackend, patchPart } from "../../../shared" + +type OpencodeClient = PluginInput["client"] export function replaceEmptyTextParts(messageID: string, replacementText: string): boolean { if (isSqliteBackend()) { - log("[session-recovery] Disabled on SQLite backend: replaceEmptyTextParts") + log("[session-recovery] Disabled on SQLite backend: replaceEmptyTextParts (use async variant)") return false } @@ -40,6 +43,38 @@ export function replaceEmptyTextParts(messageID: string, replacementText: string return anyReplaced } +export async function replaceEmptyTextPartsAsync( + client: OpencodeClient, + sessionID: string, + messageID: string, + replacementText: string +): Promise { + try { + const response = await client.session.messages({ path: { id: sessionID } }) + const messages = (response.data ?? []) as MessageData[] + + const targetMsg = messages.find((m) => m.info?.id === messageID) + if (!targetMsg?.parts) return false + + let anyReplaced = false + for (const part of targetMsg.parts) { + if (part.type === "text" && !part.text?.trim() && part.id) { + const patched = await patchPart(client, sessionID, messageID, part.id, { + ...part, + text: replacementText, + synthetic: true, + }) + if (patched) anyReplaced = true + } + } + + return anyReplaced + } catch (error) { + log("[session-recovery] replaceEmptyTextPartsAsync failed", { error: String(error) }) + return false + } +} + export function findMessagesWithEmptyTextParts(sessionID: string): string[] { const messages = readMessages(sessionID) const result: string[] = [] @@ -59,3 +94,24 @@ export function findMessagesWithEmptyTextParts(sessionID: string): string[] { return result } + +export async function findMessagesWithEmptyTextPartsFromSDK( + client: OpencodeClient, + sessionID: string +): Promise { + try { + const response = await client.session.messages({ path: { id: sessionID } }) + const messages = (response.data ?? []) as MessageData[] + const result: string[] = [] + + for (const msg of messages) { + if (!msg.parts || !msg.info?.id) continue + const hasEmpty = msg.parts.some((p) => p.type === "text" && !p.text?.trim()) + if (hasEmpty) result.push(msg.info.id) + } + + return result + } catch { + return [] + } +}