125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
import { join } from "node:path"
|
|
import type { PluginInput } from "@opencode-ai/plugin"
|
|
import { PART_STORAGE, THINKING_TYPES } from "../constants"
|
|
import type { MessageData } from "../types"
|
|
import { readMessages } from "./messages-reader"
|
|
import { readParts } from "./parts-reader"
|
|
import { log, isSqliteBackend, patchPart } from "../../../shared"
|
|
import { normalizeSDKResponse } from "../../../shared"
|
|
|
|
type OpencodeClient = PluginInput["client"]
|
|
|
|
function findLastThinkingContent(sessionID: string, beforeMessageID: string): string {
|
|
const messages = readMessages(sessionID)
|
|
|
|
const currentIndex = messages.findIndex((message) => message.id === beforeMessageID)
|
|
if (currentIndex === -1) return ""
|
|
|
|
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
const message = messages[i]
|
|
if (message.role !== "assistant") continue
|
|
|
|
const parts = readParts(message.id)
|
|
for (const part of parts) {
|
|
if (THINKING_TYPES.has(part.type)) {
|
|
const thinking = (part as { thinking?: string; text?: string }).thinking
|
|
const reasoning = (part as { thinking?: string; text?: string }).text
|
|
const content = thinking || reasoning
|
|
if (content && content.trim().length > 0) {
|
|
return content
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
export function prependThinkingPart(sessionID: string, messageID: string): boolean {
|
|
if (isSqliteBackend()) {
|
|
log("[session-recovery] Disabled on SQLite backend: prependThinkingPart (use async variant)")
|
|
return false
|
|
}
|
|
|
|
const partDir = join(PART_STORAGE, messageID)
|
|
|
|
if (!existsSync(partDir)) {
|
|
mkdirSync(partDir, { recursive: true })
|
|
}
|
|
|
|
const previousThinking = findLastThinkingContent(sessionID, messageID)
|
|
|
|
const partId = `prt_0000000000_${messageID}_thinking`
|
|
const part = {
|
|
id: partId,
|
|
sessionID,
|
|
messageID,
|
|
type: "thinking",
|
|
thinking: previousThinking || "[Continuing from previous reasoning]",
|
|
synthetic: true,
|
|
}
|
|
|
|
try {
|
|
writeFileSync(join(partDir, `${partId}.json`), JSON.stringify(part, null, 2))
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function findLastThinkingContentFromSDK(
|
|
client: OpencodeClient,
|
|
sessionID: string,
|
|
beforeMessageID: string
|
|
): Promise<string> {
|
|
try {
|
|
const response = await client.session.messages({ path: { id: sessionID } })
|
|
const messages = normalizeSDKResponse(response, [] as MessageData[], { preferResponseOnMissingData: true })
|
|
|
|
const currentIndex = messages.findIndex((m) => m.info?.id === beforeMessageID)
|
|
if (currentIndex === -1) return ""
|
|
|
|
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
const msg = messages[i]
|
|
if (msg.info?.role !== "assistant") continue
|
|
if (!msg.parts) continue
|
|
|
|
for (const part of msg.parts) {
|
|
if (part.type && THINKING_TYPES.has(part.type)) {
|
|
const content = part.thinking || part.text
|
|
if (content && content.trim().length > 0) return content
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
return ""
|
|
}
|
|
return ""
|
|
}
|
|
|
|
export async function prependThinkingPartAsync(
|
|
client: OpencodeClient,
|
|
sessionID: string,
|
|
messageID: string
|
|
): Promise<boolean> {
|
|
const previousThinking = await findLastThinkingContentFromSDK(client, sessionID, messageID)
|
|
|
|
const partId = `prt_0000000000_${messageID}_thinking`
|
|
const part: Record<string, unknown> = {
|
|
id: partId,
|
|
sessionID,
|
|
messageID,
|
|
type: "thinking",
|
|
thinking: previousThinking || "[Continuing from previous reasoning]",
|
|
synthetic: true,
|
|
}
|
|
|
|
try {
|
|
return await patchPart(client, sessionID, messageID, partId, part)
|
|
} catch (error) {
|
|
log("[session-recovery] prependThinkingPartAsync failed", { error: String(error) })
|
|
return false
|
|
}
|
|
}
|