The DCP pruning modules were using a hardcoded path (~/.config/opencode/sessions) that doesn't exist. Sessions are actually stored at ~/.local/share/opencode/storage/message. All pruning modules now import MESSAGE_STORAGE from hook-message-injector, which uses the correct path via getOpenCodeStorageDir(). This fixes the issue where DCP would fail with 'message dir not found' when trying to recover from token limit errors. 🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode)
153 lines
3.5 KiB
TypeScript
153 lines
3.5 KiB
TypeScript
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
import { join } from "node:path"
|
|
import type { PruningState, ErroredToolCall } from "./pruning-types"
|
|
import { estimateTokens } from "./pruning-types"
|
|
import { log } from "../../shared/logger"
|
|
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
|
|
|
export interface PurgeErrorsConfig {
|
|
enabled: boolean
|
|
turns: number
|
|
protectedTools?: string[]
|
|
}
|
|
|
|
interface ToolPart {
|
|
type: string
|
|
callID?: string
|
|
tool?: string
|
|
state?: {
|
|
input?: unknown
|
|
output?: string
|
|
status?: string
|
|
}
|
|
}
|
|
|
|
interface MessagePart {
|
|
type: string
|
|
parts?: ToolPart[]
|
|
}
|
|
|
|
function getMessageDir(sessionID: string): string | null {
|
|
if (!existsSync(MESSAGE_STORAGE)) return null
|
|
|
|
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 null
|
|
}
|
|
|
|
function readMessages(sessionID: string): MessagePart[] {
|
|
const messageDir = getMessageDir(sessionID)
|
|
if (!messageDir) return []
|
|
|
|
const messages: MessagePart[] = []
|
|
|
|
try {
|
|
const files = readdirSync(messageDir).filter(f => f.endsWith(".json"))
|
|
for (const file of files) {
|
|
const content = readFileSync(join(messageDir, file), "utf-8")
|
|
const data = JSON.parse(content)
|
|
if (data.parts) {
|
|
messages.push(data)
|
|
}
|
|
}
|
|
} catch {
|
|
return []
|
|
}
|
|
|
|
return messages
|
|
}
|
|
|
|
export function executePurgeErrors(
|
|
sessionID: string,
|
|
state: PruningState,
|
|
config: PurgeErrorsConfig,
|
|
protectedTools: Set<string>
|
|
): number {
|
|
if (!config.enabled) return 0
|
|
|
|
const messages = readMessages(sessionID)
|
|
|
|
let currentTurn = 0
|
|
|
|
for (const msg of messages) {
|
|
if (!msg.parts) continue
|
|
|
|
for (const part of msg.parts) {
|
|
if (part.type === "step-start") {
|
|
currentTurn++
|
|
}
|
|
}
|
|
}
|
|
|
|
state.currentTurn = currentTurn
|
|
|
|
let turnCounter = 0
|
|
let prunedCount = 0
|
|
let tokensSaved = 0
|
|
|
|
for (const msg of messages) {
|
|
if (!msg.parts) continue
|
|
|
|
for (const part of msg.parts) {
|
|
if (part.type === "step-start") {
|
|
turnCounter++
|
|
continue
|
|
}
|
|
|
|
if (part.type !== "tool" || !part.callID || !part.tool) continue
|
|
|
|
if (protectedTools.has(part.tool)) continue
|
|
|
|
if (config.protectedTools?.includes(part.tool)) continue
|
|
|
|
if (state.toolIdsToPrune.has(part.callID)) continue
|
|
|
|
if (part.state?.status !== "error") continue
|
|
|
|
const turnAge = currentTurn - turnCounter
|
|
|
|
if (turnAge >= config.turns) {
|
|
state.toolIdsToPrune.add(part.callID)
|
|
prunedCount++
|
|
|
|
const input = part.state.input
|
|
if (input) {
|
|
tokensSaved += estimateTokens(JSON.stringify(input))
|
|
}
|
|
|
|
const errorInfo: ErroredToolCall = {
|
|
callID: part.callID,
|
|
toolName: part.tool,
|
|
turn: turnCounter,
|
|
errorAge: turnAge,
|
|
}
|
|
|
|
state.erroredTools.set(part.callID, errorInfo)
|
|
|
|
log("[pruning-purge-errors] pruned old error", {
|
|
tool: part.tool,
|
|
callID: part.callID,
|
|
turn: turnCounter,
|
|
errorAge: turnAge,
|
|
threshold: config.turns,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
log("[pruning-purge-errors] complete", {
|
|
prunedCount,
|
|
tokensSaved,
|
|
currentTurn,
|
|
threshold: config.turns,
|
|
})
|
|
|
|
return prunedCount
|
|
}
|