import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs" import { homedir } from "node:os" import { join } from "node:path" import { xdgData } from "xdg-basedir" let OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage") // Fix for macOS where xdg-basedir points to ~/Library/Application Support // but OpenCode (cli) uses ~/.local/share if (process.platform === "darwin" && !existsSync(OPENCODE_STORAGE)) { const localShare = join(homedir(), ".local", "share", "opencode", "storage") if (existsSync(localShare)) { OPENCODE_STORAGE = localShare } } const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message") const PART_STORAGE = join(OPENCODE_STORAGE, "part") const TRUNCATION_MESSAGE = "[TOOL RESULT TRUNCATED - Context limit exceeded. Original output was too large and has been truncated to recover the session. Please re-run this tool if you need the full output.]" interface StoredToolPart { id: string sessionID: string messageID: string type: "tool" callID: string tool: string state: { status: "pending" | "running" | "completed" | "error" input: Record output?: string error?: string time?: { start: number end?: number compacted?: number } } truncated?: boolean originalSize?: number } export interface ToolResultInfo { partPath: string partId: string messageID: string toolName: string outputSize: number } function getMessageDir(sessionID: string): string { if (!existsSync(MESSAGE_STORAGE)) return "" const directPath = join(MESSAGE_STORAGE, sessionID) if (existsSync(directPath)) { return directPath } for (const dir of readdirSync(MESSAGE_STORAGE)) { const sessionPath = join(MESSAGE_STORAGE, dir, sessionID) if (existsSync(sessionPath)) { return sessionPath } } return "" } function getMessageIds(sessionID: string): string[] { const messageDir = getMessageDir(sessionID) if (!messageDir || !existsSync(messageDir)) return [] const messageIds: string[] = [] for (const file of readdirSync(messageDir)) { if (!file.endsWith(".json")) continue const messageId = file.replace(".json", "") messageIds.push(messageId) } return messageIds } export function findToolResultsBySize(sessionID: string): ToolResultInfo[] { const messageIds = getMessageIds(sessionID) const results: ToolResultInfo[] = [] for (const messageID of messageIds) { const partDir = join(PART_STORAGE, messageID) if (!existsSync(partDir)) continue for (const file of readdirSync(partDir)) { if (!file.endsWith(".json")) continue try { const partPath = join(partDir, file) const content = readFileSync(partPath, "utf-8") const part = JSON.parse(content) as StoredToolPart if (part.type === "tool" && part.state?.output && !part.truncated) { results.push({ partPath, partId: part.id, messageID, toolName: part.tool, outputSize: part.state.output.length, }) } } catch { continue } } } return results.sort((a, b) => b.outputSize - a.outputSize) } export function findLargestToolResult(sessionID: string): ToolResultInfo | null { const results = findToolResultsBySize(sessionID) return results.length > 0 ? results[0] : null } export function truncateToolResult(partPath: string): { success: boolean toolName?: string originalSize?: number } { try { const content = readFileSync(partPath, "utf-8") const part = JSON.parse(content) as StoredToolPart if (!part.state?.output) { return { success: false } } const originalSize = part.state.output.length const toolName = part.tool part.truncated = true part.originalSize = originalSize part.state.output = TRUNCATION_MESSAGE if (!part.state.time) { part.state.time = { start: Date.now() } } part.state.time.compacted = Date.now() writeFileSync(partPath, JSON.stringify(part, null, 2)) return { success: true, toolName, originalSize } } catch { return { success: false } } } export function getTotalToolOutputSize(sessionID: string): number { const results = findToolResultsBySize(sessionID) return results.reduce((sum, r) => sum + r.outputSize, 0) } export function countTruncatedResults(sessionID: string): number { const messageIds = getMessageIds(sessionID) let count = 0 for (const messageID of messageIds) { const partDir = join(PART_STORAGE, messageID) if (!existsSync(partDir)) continue for (const file of readdirSync(partDir)) { if (!file.endsWith(".json")) continue try { const content = readFileSync(join(partDir, file), "utf-8") const part = JSON.parse(content) if (part.truncated === true) { count++ } } catch { continue } } } return count } export interface AggressiveTruncateResult { success: boolean sufficient: boolean truncatedCount: number totalBytesRemoved: number targetBytesToRemove: number truncatedTools: Array<{ toolName: string; originalSize: number }> } export function truncateUntilTargetTokens( sessionID: string, currentTokens: number, maxTokens: number, targetRatio: number = 0.8, charsPerToken: number = 4 ): AggressiveTruncateResult { const targetTokens = Math.floor(maxTokens * targetRatio) const tokensToReduce = currentTokens - targetTokens const charsToReduce = tokensToReduce * charsPerToken if (tokensToReduce <= 0) { return { success: true, sufficient: true, truncatedCount: 0, totalBytesRemoved: 0, targetBytesToRemove: 0, truncatedTools: [], } } const results = findToolResultsBySize(sessionID) if (results.length === 0) { return { success: false, sufficient: false, truncatedCount: 0, totalBytesRemoved: 0, targetBytesToRemove: charsToReduce, truncatedTools: [], } } let totalRemoved = 0 let truncatedCount = 0 const truncatedTools: Array<{ toolName: string; originalSize: number }> = [] for (const result of results) { const truncateResult = truncateToolResult(result.partPath) if (truncateResult.success) { truncatedCount++ const removedSize = truncateResult.originalSize ?? result.outputSize totalRemoved += removedSize truncatedTools.push({ toolName: truncateResult.toolName ?? result.toolName, originalSize: removedSize, }) } } const sufficient = totalRemoved >= charsToReduce return { success: truncatedCount > 0, sufficient, truncatedCount, totalBytesRemoved: totalRemoved, targetBytesToRemove: charsToReduce, truncatedTools, } }